使用程序集加快一点测试操作

时间:2014-07-13 13:25:16

标签: c++ performance assembly compiler-construction compiler-optimization

我遇到了性能问题 - 我根本无法击败编译器生成代码的发布版本速度。它慢了25%。我编写的函数在我的测试中被调用了大约2000万次,因此让它运行得更快就能获得回报。

C ++中的代码非常简单:

static inline char GetBit(char *data, size_t bit)
{ 
    return 0 != (data[bit / 8] & (1 << (bit % 8))); 
}

这是我为64位MASM编写的版本:

mov   rax, rdx  
mov   r10, 8h
xor   rdx, rdx  
div   rax, r10  
mov   al, byte ptr [rax+rcx]  
mov   bl, 1h
mov   cl, dl  
shl   bl, cl  
and   al, bl  
shr   al, cl  
ret 

嗯,我不是一个汇编人员,但我认为编译器只需要创建更好的汇编就能使代码速度提高25%。所以诀窍是[可能]在函数调用中。它尊重C ++代码的内联关键字并且不生成任何调用,但我无法使其适用于asm代码:

extern "C" inline char GetBitAsm(char *data, size_t bit);

我已经使用 dumpbin 对代码进行了反汇编,我可以清楚地看到我的代码+函数调用。虽然没有为编译器的版本生成调用:

mov   rdx, qword ptr [bit]  
mov   rcx, qword ptr [data]  
call  GetBitAsm (013F588EFDh)  
mov   byte ptr [isbit], al  

还有2个读数和1个写入内存,而在编译器生成的内容中,可能只有1个读数。我读到某处 div 操作码大约需要20个周期,而单个内存访问至少需要100个周期。所以从内存中删除mov rdx和mov rcx,用父函数中寄存器的值替换它们,我认为会做的伎俩

问题:

  1. 这真的是它运行得如此之慢的原因吗?

  2. 如何在发布版本中以asm inline编写函数?

  3. 如何进一步增强汇编代码,使其更快?

3 个答案:

答案 0 :(得分:5)

相对于任何编译器的内联代码,无法听到函数调用和汇编代码中的DIV指令会破坏性能。单独的函数调用开销可能更大程度上是编译器代码平均占用的周期数。 DIV指令可能会高出几倍。

现代处理器上的内存访问通常是免费的,因为它们可以从处理器的缓存中得到满足。在您的汇编版本中,您的内存访问平均需要花费0个周期,因为您的代码可能足够慢,以至于处理器可以在需要访问内存之前轻松地将内存预取到其缓存中。另一方面,编译器的代码可能足够快,以至于它可以比处理器可以获取它更快地从内存中读取值。它必须定期停止等待提取编译。因此,虽然编译器代码中的内存访问周期时间平均会更高,但这只是因为它的优化程度要高得多。

解决问题的最佳方法是让编译器进行优化。坦率地说,它似乎知道如何生成比你更好的代码。即使是汇编专家也很难改进编译器,并且需要在更广泛的范围内查看问题而不仅仅是指令选择这一功能。

如果你仍然使用自己的汇编代码,那么使用编译器的内联汇编功能,并摆脱DIV指令。它仍然不会像编译器的版本那样表现出色,但是应该让它更接近它。

答案 1 :(得分:1)

我会在这里做一个长镜头并猜测你想要做什么,所以请耐心等待:

有一些东西让我对你的代码感兴趣,(C ++和汇编程序)第一个就是其他人提到你使用div和mod。这些操作相当慢,并且您无法与编译器竞争的原因之一是,它很可能会优化这些操作。

你正在使用2的幂,计算机是为了使用2的幂。这意味着这相当于你的代码:

static inline char GetBit(char *data, size_t bit)
{ 
    return 0 != (data[bit >> 3] & (1 << (bit & 0x07))); 
}

您可以使用它来改进您的装配,但这不会带来很大的性能提升。

另一方面,如果您的目标是加快代码速度,我会建议您进行以下更改:

  1. 在大型位掩码中,将基本类型更改为处理器本机大小,即32位计算机的uint32_t和64位计算机的uint64_t。

  2. 此外,将getBit()函数分为两个函数:getWord()和getBit()。

    getWord()应该是很长的路线:

    static inline uint32_t getWord(const uint32_t *data, size_t bit) { 
        return data[ bit / sizeof(*data)*8 ]; // Again, the compiler will most 
                                              // likely pick up that this is a 
                                              // division by a power of 2 and 
                                              // optimize accordingly.
                                              // Check to be certain.
    }
    
    static inline uint32_t getBit(const uint32_t *data, size_t bit) { 
         return getWord(data, bit) & (1 << (bit & (sizeof(*data)*8 - 1)); 
         // Or just % like above, check which is faster. 
    }
    
  3. 如果使用此位掩码重写代码,应该会实现真正的加速:

    1. 如果你在缓冲区中跳了很多,你可能只会从上面的建议中稍微改进一下。

    2. 但是,如果您以线性方式迭代数据,我建议您将代码更改为:

       uint32_t mask = 1;
       uint32_t word;
       for ( int bit = 0; bit < 2048; i++) {
           word = getWord(buffer, i); // You could also move this outside a smaller loop, but I'm not sure it's worth it.
           if (word & mask) {
               cout << "Bit " << bit << " is set." << endl;
           }
      
           // Most modern compilers will recognize the following as a ROL 
           // (ROtational Left shift) and replace it with one instruction.
           mask = (mask << 1 | mask >> (sizeof(mask)*8-1)); 
       }
      
    3. 这个好主意的原因是处理器已经过优化,可以使用原生大小的整数,可以避免对齐问题,升级寄存器中的值等。您可能还会注意到,通过使用掩码一直在外面循环你避免额外的转移/分裂,因为我们只是让面具在它填满时滚动。

答案 2 :(得分:0)

除了已经说过的所有内容之外,你还必须关注&#34;内联&#34;功能:

您可以尝试删除&#34;内联&#34;从(纯C / C ++)函数中将函数移动到另一个C文件,这样您就可以确定编译器没有内联函数。您将看到该函数运行得慢得多。

原因:当一个函数是&#34; inline&#34;编译器可以优化很多。当一个函数不是&#34; inline&#34;编译器必须将函数参数存储在堆栈中(使用&#34; push&#34;)并执行&#34;调用&#34;指令。这将花费大量时间并使代码比“内联”#34;功能

对于小块代码,这些操作所需的时间远远超过使用汇编程序代码节省的时间!