编译器选择不使用REP MOVSB指令进行字节数组移动

时间:2018-07-01 05:02:03

标签: c++ assembly visual-c++ x86 compiler-optimization

我正在检查使用最新版本的VS 2017 C ++编译器完成的项目的Release版本。我很好奇编译器为什么选择构建以下代码片段:

//ncbSzBuffDataUsed of type INT32

UINT8* pDst = (UINT8*)(pMXB + 1);
UINT8* pSrc = (UINT8*)pDPE;
for(size_t i = 0; i < (size_t)ncbSzBuffDataUsed; i++)
{
    pDst[i] = pSrc[i];
}

例如:

enter image description here

        UINT8* pDst = (UINT8*)(pMXB + 1);
        UINT8* pSrc = (UINT8*)pDPE;
        for(size_t i = 0; i < (size_t)ncbSzBuffDataUsed; i++)
00007FF66441251E 4C 63 C2             movsxd      r8,edx  
00007FF664412521 4C 2B D1             sub         r10,rcx  
00007FF664412524 0F 1F 40 00          nop         dword ptr [rax]  
00007FF664412528 0F 1F 84 00 00 00 00 00 nop         dword ptr [rax+rax]  

00007FF664412530 41 0F B6 04 0A       movzx       eax,byte ptr [r10+rcx]  
        {
            pDst[i] = pSrc[i];
00007FF664412535 88 01                mov         byte ptr [rcx],al  
00007FF664412537 48 8D 49 01          lea         rcx,[rcx+1]  
00007FF66441253B 49 83 E8 01          sub         r8,1  
00007FF66441253F 75 EF                jne         _logDebugPrint_in_MainXchgBuffer+0A0h (07FF664412530h)  
        }

相对于仅使用一条REP MOVSB指令?后者会不会更有效率?

2 个答案:

答案 0 :(得分:4)

编辑:首先,有一个rep movsb的内在函数,彼得·科德斯(Peter Cordes)告诉我们这里会更快,而且我相信他(我想我已经做到了)。如果要强制编译器以这种方式执行操作,请参见:__movsb()https://docs.microsoft.com/en-us/cpp/intrinsics/movsb

关于为什么编译器没有为您执行此操作,如果没有其他想法,答案可能是注册压力。要使用rep movsb,编译器必须:

  • 设置rsi(=源地址)
  • 设置rdi(=目标地址)
  • 设置rcx(=计数)
  • 发出rep movsb

因此,现在它不得不用完rep movsb指令授权的三个寄存器,并且它可能不希望这样做。具体而言,rsirdi有望在整个函数调用中保留,因此,如果编译器可以在任何特定函数的主体中使用它们,并且(在方法的初始输入时) ,至少)rcx持有this指针。

此外,使用我们看到的编译器生成的代码,r10rcx寄存器可能已经包含必需的源地址和目标地址(我们无法从您的示例中看到) ,如果这样的话,对编译器来说很方便。

实际上,您可能会看到编译器在不同情况下做出不同选择。所请求的优化类型(/O1-优化大小,而/O2-优化速度)也可能会对此产生影响。

有关x64寄存器传递约定here和x64 ABI通常here的更多信息。


编辑2 (同样受到Peter的评论启发):

编译器可能决定不对循环进行矢量化处理,因为它不知道指针是否对齐或可能重叠。没有看到更多的代码,我们无法确定。但是,鉴于OP的实际要求,这与我的回答并不严格相关。

答案 1 :(得分:0)

这并不是真正的答案,我无法将其全部塞入评论中。我只想分享我的其他发现。 (这可能仅与Visual Studio编译器有关。)

与众不同的是您如何构造循环。例如:

假设以下结构定义:

#define PCALLBACK ULONG64

#pragma pack(push)
#pragma pack(1)
typedef struct {
    ULONG64 ui0;

    USHORT w0;
    USHORT w1;

    //Followed by:
    //  PCALLBACK[] 'array' - variable size array
}DPE;
#pragma pack(pop)

(1)构造for循环的常规方法。在较大的序列化函数中间的某处调用以下代码块:

PCALLBACK* pDstClbks = (PCALLBACK*)(pDPE + 1);
for(size_t i = 0; i <  (size_t)info.wNumCallbackFuncs; i++)
{
    pDstClbks[i] = info.callbackFuncs[i];
}

正如本页答案中提到的,很明显,编译器因寄存器不足而产生了以下怪异现象(请参阅如何将rax用作循环结束限制或{{1 }}可能被明确排除在循环之外的指令。)

movzx eax,word ptr [r13]

(2)因此,如果我将其重新编写为不太熟悉的C模式

    PCALLBACK* pDstClbks = (PCALLBACK*)(pDPE + 1);
00007FF7029327CF 48 83 C1 30          add         rcx,30h  
    for(size_t i = 0; i <  (size_t)info.wNumCallbackFuncs; i++)
00007FF7029327D3 66 41 3B 5D 00       cmp         bx,word ptr [r13]  
00007FF7029327D8 73 1F                jae         07FF7029327F9h
00007FF7029327DA 4C 8B C1             mov         r8,rcx  
00007FF7029327DD 4C 2B F1             sub         r14,rcx  
    {
        pDstClbks[i] = info.callbackFuncs[i];
00007FF7029327E0 4B 8B 44 06 08       mov         rax,qword ptr [r14+r8+8]  
00007FF7029327E5 48 FF C3             inc         rbx  
00007FF7029327E8 49 89 00             mov         qword ptr [r8],rax  
00007FF7029327EB 4D 8D 40 08          lea         r8,[r8+8]  
00007FF7029327EF 41 0F B7 45 00       movzx       eax,word ptr [r13]  
00007FF7029327F4 48 3B D8             cmp         rbx,rax  
00007FF7029327F7 72 E7                jb          07FF7029327E0h
    }
00007FF7029327F9 45 0F B7 C7          movzx       r8d,r15w  

这会产生更明智的机器代码(在同一编译器,同一函数,同一项目中):

PCALLBACK* pDstClbks = (PCALLBACK*)(pDPE + 1);
PCALLBACK* pEndDstClbks = pDstClbks + (size_t)info.wNumCallbackFuncs;
for(PCALLBACK* pScrClbks = info.callbackFuncs; 
    pDstClbks < pEndDstClbks; 
    pScrClbks++, pDstClbks++)
{
    *pDstClbks = *pScrClbks;
}