我想知道它是否是语言的设计(或者可能是共享库机制)或者它只是编译器的弱点,但我想知道为什么在第一种情况下代码得到优化(无用的调用被删除),但不是在第二种情况下。
我有一个主机程序(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方法完全相同。
- 当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)]
}
- 但是如果我不定义宏,则使用共享库的条形定义,并且在这种情况下,无用的调用不会被优化掉,正如我们在反汇编中看到的那样:
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
内联,因此没有执行时间损失。