一个简单的for循环的意外无意义优化尝试(在Visual C++ 2017中)

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 的成员,它也是地址.
  • 在地址xxx320中,将ptr其加载到寄存器r8中.
  • 从xxx324到xxx338是主循环.将ptr [i]的值加载到r9d中,递增rax(ptr)并将r9d添加到Lambda对象的sum-member.

我的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)

它解决了原始来源的问题:

  • ptrr9仅在方法的开头加载.
  • rdx(lambda的地址)不会被缓存,只是存储在rdx寄存器中.
  • 主循环只迭代xxx350 tp xxx35D(加载ptr [i]进入r8d,递增i in rax,将ptr [i]缓存r8d到Lambda成员求和的地址中rdx,检查rax> = size in address atrcx

我的第三个问题是:

如果机器代码实际上以与原始版本相同的速度运行,那该怎么办?每个循环只有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;
}
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中这些是毫秒.)

mks*_*eve 0

为什么地址 xxx320 是循环的一部分?r8 未更改,并且 ptr 成员未标记为易失性。xxx338中的jmp不应该指向xxx324吗?

为什么每次循环时lambda地址都会从rdx缓存到xxx312中的[f]并恢复到xxx333中的rdx?rdx 没有改变,那么为什么编译器会重新加载它呢?

r8 和 rdx 都是易失性寄存器,它们不会在函数调用后继续存在,因此在某些时候优化器会认为对 f() 的调用将导致值溢出。

当它确实优化了实际的函数调用时,它可能不会将代码作为单个指令流进行检查并删除冗余的重新加载。

性能是由add函数驱动的,这意味着额外的不需要的操作不会驱动代码的性能。