为什么 MSVC 编译器/优化器不会删除共享库中重复的函数调用,而是复制整个函数体?



我想知道它是否是语言的设计(或者可能是共享库机制)或者它只是编译器的弱点,但我想知道为什么在第一种情况下代码得到优化(无用的调用被删除),但不是在第二种情况下。

我有一个主机程序(test_host)导入一个共享库(test_dll)。

test_dll.h:

#ifdef TESTDLL_EXPORTS
#define TESTDLL_API __declspec(dllexport)
#else
#define TESTDLL_API __declspec(dllimport)
#endif
class TESTDLL_API TestClass {
public:
TestClass(void);
int foo(int a, int b, int c);
};
#ifndef NOT_USE_BAR_FROM_DLL
TESTDLL_API int bar(TestClass* fooClass, int a, int b, int c);
#endif
#endif

test_dll.cpp:

int TestClass::foo(int a, int b, int c)
{
return std::printf("%d + %d - %d = %dn", a, b, c, (a + b - c));
}
TESTDLL_API int bar(TestClass* fooClass, int a, int b, int c)
{
return fooClass->foo(a, b, c);
}

test_host.cpp:

#ifdef NOT_USE_BAR_FROM_DLL
int bar(TestClass* fooClass, int a, int b, int c)
{
return fooClass->foo(a, b, c);
}
#endif
int main()
{
TestClass inst;
inst.foo(8, 6, 4);
bar(&inst, 4, 7, 5);
}

焦点在函数,它除了使用给定的参数从TestClass实例调用foo方法之外,什么也不做。由于方法接受隐藏的this参数,因此调用bar与调用foo方法完全相同。

  1. 当I#define NOT_USE_BAR_FROM_DLL(在包含test_dll.h之前)时,编译器使用本地(来自test_host) bar定义并看到它相当于直接调用foo方法,因此删除对函数的调用而直接调用foo方法;我们可以看到在拆解中:
int main()
{
00F91000  push        ebp  
00F91001  mov         ebp,esp  
00F91003  push        ecx  
TestClass inst;
00F91004  lea         ecx,[inst]  
00F91007  call        dword ptr [__imp_TestClass::TestClass (0F920BCh)]  
inst.foo(8, 6, 4);
00F9100D  push        4  
00F9100F  push        6  
00F91011  push        8  
00F91013  lea         ecx,[inst]  
00F91016  call        dword ptr [__imp_TestClass::foo (0F920C0h)]  
bar(&inst, 4, 7, 5);
00F9101C  push        5  
00F9101E  push        7  
00F91020  push        4  
00F91022  lea         ecx,[inst]  
00F91025  call        dword ptr [__imp_TestClass::foo (0F920C0h)]  
}
  1. 但是如果我不定义宏,则使用共享库的条形定义,并且在这种情况下,无用的调用不会被优化掉,正如我们在反汇编中看到的那样:
int main()
{
01091002  in          al,dx  
01091003  push        ecx  
TestClass inst;
01091004  lea         ecx,[inst]  
01091007  call        dword ptr [__imp_TestClass::TestClass (010920BCh)]  
inst.foo(8, 6, 4);
0109100D  push        4  
0109100F  push        6  
01091011  push        8  
01091013  lea         ecx,[inst]  
01091016  call        dword ptr [__imp_TestClass::foo (010920C0h)]  
bar(&inst, 4, 7, 5);
0109101C  push        5  
0109101E  push        7  
01091020  lea         eax,[inst]  
01091023  push        4  
01091025  push        eax  
01091026  call        dword ptr [__imp_bar (010920C4h)]  
0109102C  add         esp,10h  
}

我们看到这里调用了bar函数,它的地址与foo方法不同,所以bar有自己的代码。有人知道为什么它不会优化调用时,函数驻留在共享库?我的意思是,编译器至少可以将bar函数的导入地址设置为TestClass::foo的地址,因为调用TestClass::foo与调用bar完全相同。

更糟糕的是,如果我查看bar反汇编代码的内容(在共享库中),它没有调用foo方法,而是包含了foo方法体的副本。一开始我想删除一个额外的调用,但是如果整个代码体都是重复的,那就更糟了。

我做了这个测试,因为我正在寻找一个干净的方法来制作一个c++库(使用OOP API),但也在其中添加了一个C API,以一种可移植的方式将函数调用转换为类方法(这样的库可以在C和c++ OOP方式中使用)。但是如果编译器不能做这些基本的优化,那就有点遗憾了。

当然函数调用是可以忽略不计的,但我想知道是否有人对此有一个想法,如果编译器的行为只是严格的ABI要求,或者如果我可以使用编译器选项来强制优化,保持干净和可移植,ABI正确(无论如何,我不知道这种行为是否是msvc特定的,或者如果gcc, llvm和其他人做同样的)。

DLL中foo和bar的反汇编:

int TestClass::foo(int a, int b, int c)
{
51051070  push        ebp  
51051071  mov         ebp,esp  
return std::printf("%d + %d - %d = %dn", a, b, c, (a + b - c));
51051073  mov         edx,dword ptr [c]  
51051076  mov         ecx,dword ptr [a]  
51051079  mov         eax,dword ptr [b]  
5105107C  sub         ecx,edx  
5105107E  add         ecx,eax  
51051080  push        ecx  
51051081  push        edx  
51051082  push        eax  
51051083  push        dword ptr [a]  
51051086  push        offset string "%d + %d - %d = %dn" (510520A4h)  
5105108B  call        printf (51051030h)  
51051090  add         esp,14h  
}
TESTDLL_API int bar(TestClass* fooClass, int a, int b, int c)
{
510510A0  push        ebp  
510510A1  mov         ebp,esp  
return fooClass->foo(a, b, c);
510510A3  mov         edx,dword ptr [c]  
510510A6  mov         ecx,dword ptr [a]  
510510A9  mov         eax,dword ptr [b]  
510510AC  sub         ecx,edx  
510510AE  add         ecx,eax  
510510B0  push        ecx  
510510B1  push        edx  
510510B2  push        eax  
510510B3  push        dword ptr [a]  
510510B6  push        offset string "%d + %d - %d = %dn" (510520A4h)  
510510BB  call        printf (51051030h)  
510510C0  add         esp,14h  
}

换句话说,我希望库中的bar函数的行为像test_host中的一样,即作为一个"符号链接"。到foo方法;要么使用相同的导出地址,在本例中,test_dll的导出表将如下所示:

0x000920C0 foo@TestClass
0x000920C0 bar

或者告诉编译器在编译test_host时知道导入的"bar"可以忽略,而使用TestClass::foo(但我猜它不会从C主机工作,因为TestClass不能被声明,编译器将无法知道bar可以被理解为TestClass::foo)。

这样,在C和c++主机中使用我的库将具有相同的函数/方法调用成本。

可能吗?

不能合并这两个函数,因为它们使用不同的调用约定。非静态成员函数使用__thiscall,而独立函数使用__cdecl。

您应该注意到,在您的示例中,bar的编译代码不包括对foo的实际调用,而是将foo内联,因此没有执行时间损失。

最新更新