提问人:Andreas H. 提问时间:8/6/2017 最后编辑:Andreas H. 更新时间:10/13/2017 访问量:320
简单 for 循环的意外无意义优化尝试(在 Visual C++ 2017 中)
Unexpected senseless optimization tries of a simple for loop (in Visual C++ 2017)
问:
我正在玩 Visual C++ 2017 编译器,做一些关于如何实现各种东西以从代码中获得最大性能的测试,当我遇到一个我没有预料到也无法解释的行为时。
我创建了一个简单的 foreach 方法来处理容器中的所有值。容器本身仅存储 a 和 .size_t size
int *ptr
这是 foreach 方法的代码,在 f 中采用 Lambda:
template<class F>
__declspec(noinline) void foreach(F f)
{
for (size_t i = 0; i < size; ++i)
f(ptr[i]);
}
我用
int sum = 0;
v.foreach([&](int item) { sum += item; });
编译时,二进制文件的一部分如下所示:
for (size_t i = 0; i < size; ++i)
00007FF6B3382310 xor eax,eax
00007FF6B3382312 mov qword ptr [f],rdx
00007FF6B3382317 cmp qword ptr [rcx],rax
00007FF6B338231A jbe MyVector::foreach<<lambda_c1957c9a484ac2f96c41b63c392e4950> >+2Ah (07FF6B338233Ah)
00007FF6B338231C nop dword ptr [rax]
f(ptr[i]);
00007FF6B3382320 mov r8,qword ptr [rcx+8]
00007FF6B3382324 mov r9d,dword ptr [r8+rax*4]
for (size_t i = 0; i < size; ++i)
00007FF6B3382328 inc rax
f(ptr[i]);
00007FF6B338232B add dword ptr [rdx],r9d
for (size_t i = 0; i < size; ++i)
00007FF6B338232E cmp rax,qword ptr [rcx]
00007FF6B3382331 jae MyVector::foreach<<lambda_c1957c9a484ac2f96c41b63c392e4950> >+2Ah (07FF6B338233Ah)
00007FF6B3382333 mov rdx,qword ptr [f]
00007FF6B3382338 jmp MyVector::foreach<<lambda_c1957c9a484ac2f96c41b63c392e4950> >+10h (07FF6B3382320h)
}
00007FF6B338233A ret
分析:
- 的地址也是如此,也是 的地址(对象的第一个成员也是如此)。
rcx
size
this
size
rdx
看作是 Lambda 函子的地址,如果是 Lambda 的成员,它也是地址。sum
- 在地址 xxx320 中,加载到寄存器 r8 中。
ptr
- 从 xxx324 到 xxx338 是主循环。将 ptr[i] 的值加载到 r9d 中,rax (ptr) 递增,并将 r9d 添加到 Lambda 对象的 sum 成员中。
我的 3 个问题中的前 2 个是:
为什么地址 xxx320 是循环的一部分?R8 未更改,PTR 成员未标记 。xxx338 中的 jmp 不应该指向 xxx324 吗?
volatile
为什么 lambda 地址在每个循环中都从 xxx312 中的 to 缓存并恢复到 xxx333 中?RDX 没有改变,那么为什么编译器要重新加载它呢?
rdx
[f]
rdx
我试图摆脱这些“低效率”,发现以下源代码创建了更合理的机器代码:
template<class F>
__declspec(noinline) void foreach(F f)
{
register auto f2 = f;
register auto p = ptr;
for (size_t i = 0; i < size; ++i)
f2(p[i]);
}
生成的机器码是
register auto f2 = f;
register auto p = ptr;
00007FF7E79A2340 mov r9,qword ptr [rcx+8]
for (size_t i = 0; i < size; ++i)
00007FF7E79A2344 xor eax,eax
00007FF7E79A2346 cmp qword ptr [rcx],rax
00007FF7E79A2349 jbe MyCheckedVector::foreach<<lambda_27103c2606044b6f9a288cfb44283d2c> >+1Fh (07FF7E79A235Fh)
00007FF7E79A234B nop dword ptr [rax+rax]
f2(p[i]);
00007FF7E79A2350 mov r8d,dword ptr [r9+rax*4]
for (size_t i = 0; i < size; ++i)
00007FF7E79A2354 inc rax
f2(p[i]);
00007FF7E79A2357 add dword ptr [rdx],r8d
for (size_t i = 0; i < size; ++i)
00007FF7E79A235A cmp rax,qword ptr [rcx]
00007FF7E79A235D jb MyCheckedVector::foreach<<lambda_27103c2606044b6f9a288cfb44283d2c> >+10h (07FF7E79A2350h)
}
00007FF7E79A235F ret
它修复了原始来源的问题:
ptr
或仅在方法的开头加载。r9
rdx
(lambda 的地址)不会被缓存,而只是存储在寄存器中。rdx
- 主循环仅从 xxx350 tp xxx35D 迭代(将 ptr[i] 加载到 ,在 中增加 i,将 ptr[i] 缓存到 Lambda 成员总和中 address,检查 >= size in address at
r8d
rax
r8d
rdx
rax
rcx
我的第三个问题是:
如果机器代码实际上以与原始版本相同的速度运行,为什么会出现这部分?它每个循环只有 5 条指令,而原始循环中有 8 条指令,所有 5 条指令也都存在于原始循环中。
也许我还应该提到我的优化设置:
- 全面优化
- 内联功能扩展:任何合适的
- 内部函数:是
- 偏爱快速代码
- 全程优化
- 增强型指令集:高级矢量扩展 2 (AVX2)
我知道我应该把优化留给编译器,但有时我会出于好奇而深入研究机器代码。我真的很感激任何解释!
---更新---
我创建了一个示例,它不是 100% 的原始代码,而是创建相同的机器代码。该示例在内部循环中仅使用 1K 个值(4000 字节数据),因此内存访问不应限制性能。
#include <iostream>
#include <ctime>
#include <conio.h>
#include <algorithm>
#include <vector>
static const auto OuterLoops = 100000;
class MyContainer
{
static const auto Items = 100000000 / OuterLoops;
public:
MyContainer()
: size(Items),
ptr(new int[Items])
{
// Make sure memory is paged
std::fill(ptr, ptr + size, 0xA5);
}
template<class F> __declspec(noinline) auto foreach1(F f)
{
for (size_t i = 0; i < size; ++i)
f(ptr[i]);
}
template<class F> __declspec(noinline) auto foreach2(F f)
{
const register auto sizer = size;
const register auto fr = f;
const register auto ptrr = ptr;
for (size_t i = 0; i < sizer; ++i)
fr(ptrr[i]);
}
private:
size_t size;
int *ptr;
};
template<class F>
void measureSpeed(const char *const caption, F f)
{
std::vector<int> results(11);
for (auto& result : results)
{
for (auto a = clock(); a == clock(); );
const auto start1 = clock();
for(int i = 0; i < OuterLoops; ++i)
f();
result = clock() - start1;
}
std::sort(results.begin(), results.end());
std::cout << caption << ": " << results[results.size() / 2] << " (";
for (const auto& result : results)
std::cout << result << ' ';
std::cout << "\b)" << std::endl;
}
int main(int argc, char **argv)
{
MyContainer c;
int s1 = 0;
measureSpeed("foreach1", [&]() { c.foreach1([&](const auto v) { s1 += v; }); });
int s2 = 0;
measureSpeed("foreach2", [&]() { c.foreach2([&](const auto v) { s2 += v; }); });
if (s1 != s2)
std::cerr << "Comparing nonsense" << std::endl;
_getch();
return 0;
}
我得到的结果有点不同,但 foreach2 总是比 foreach1 慢 5% 左右:
foreach1: 195 (190 191 192 193 195 195 195 195 196 196 197)
foreach2: 207 (202 202 205 206 206 207 208 208 212 213 214)
(第一个值是所有测试运行的中位数,后跟所有测试运行的排序计时。这些值来自 clock() 并且未转换,但至少在 Visual Studio 平台工具集 v140 中,这些值是毫秒。
答:
为什么地址 xxx320 是循环的一部分?R8 未更改,PTR 成员未标记为易失性。xxx338 中的 jmp 不应该指向 xxx324 吗?
为什么 lambda 地址在 xxx312 中从 rdx 缓存到 [f],并在每个循环中恢复到 xxx333 中的 rdx?RDX 没有改变,那么为什么编译器要重新加载它呢?
R8 和 RDX 都是易失性寄存器,它们在函数调用中无法存活,因此在某些时候,优化器会认为对 f() 的调用会导致值溢出。
当它优化掉实际的函数调用时,它可能不会将代码作为单个指令流进行检查并删除冗余的重新加载。
性能是由 add 函数驱动的,这意味着额外的不需要的操作不会驱动代码的性能。
评论
__restrict