And*_* H. 7 c++ optimization assembly loops visual-c++
我正在玩Visual C++ 2017编译器做一些关于如何实现各种事情的测试,以便在遇到我没想到的行为时也能从代码中获得最大性能,也无法解释它.
我创建了一个简单的foreach方法来处理容器中的所有值.容器本身只存储一个size_t size和一个int *ptr.
这是在f中采用Lambda的foreach方法的代码:
template<class F>
__declspec(noinline) void foreach(F f)
{
for (size_t i = 0; i < size; ++i)
f(ptr[i]);
}
Run Code Online (Sandbox Code Playgroud)
我叫它
int sum = 0;
v.foreach([&](int item) { sum += item; });
Run Code Online (Sandbox Code Playgroud)
编译时,二进制文件的一部分如下所示:
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
Run Code Online (Sandbox Code Playgroud)
分析:
rcx是的地址size,并且还的地址this(如size为对象的第一成员).rdxseens是Lambda仿函数的地址,如果sum是Lambda 的成员,它也是地址.ptr其加载到寄存器r8中.我的3个问题中的前2个是:
为什么地址xxx320是循环的一部分?r8未更改,并且未标记ptr-member volatile.xxx338中的jmp不应该指向xxx324吗?
为什么拉姆达地址缓存从rdx以[f]在xxx312并恢复到rdx在每个循环中xxx333?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]);
}
Run Code Online (Sandbox Code Playgroud)
生成的机器代码是
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
Run Code Online (Sandbox Code Playgroud)
它解决了原始来源的问题:
ptr或r9仅在方法的开头加载.rdx(lambda的地址)不会被缓存,只是存储在rdx寄存器中.r8d,递增i in rax,将ptr [i]缓存r8d到Lambda成员求和的地址中rdx,检查rax> = size in address atrcx我的第三个问题是:
如果机器代码实际上以与原始版本相同的速度运行,那该怎么办?每个循环只有5条指令,而原始循环中只有8条指令,所有5条指令也存在于原始循环中.
也许我还应该提一下我的优化设置:
我知道我应该对编译器进行优化,但有时我会出于好奇而深入研究机器代码.我真的很感激任何解释!
---更新---
我创建了一个不是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;
}
Run Code Online (Sandbox Code Playgroud)
我得到的结果有点变化,但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)
Run Code Online (Sandbox Code Playgroud)
(第一个值是所有测试运行的中位数,后跟所有测试运行的排序时间.值来自clock()而未转换,但至少在Visual Studio Platform Toolset v140中这些是毫秒.)
为什么地址 xxx320 是循环的一部分?r8 未更改,并且 ptr 成员未标记为易失性。xxx338中的jmp不应该指向xxx324吗?
为什么每次循环时lambda地址都会从rdx缓存到xxx312中的[f]并恢复到xxx333中的rdx?rdx 没有改变,那么为什么编译器会重新加载它呢?
r8 和 rdx 都是易失性寄存器,它们不会在函数调用后继续存在,因此在某些时候优化器会认为对 f() 的调用将导致值溢出。
当它确实优化了实际的函数调用时,它可能不会将代码作为单个指令流进行检查并删除冗余的重新加载。
性能是由add函数驱动的,这意味着额外的不需要的操作不会驱动代码的性能。
| 归档时间: |
|
| 查看次数: |
260 次 |
| 最近记录: |