为什么我的代码会导致指令缓存未命中

Why does my code cause instruction-cache misses?

本文关键字:指令缓存 我的 代码 为什么      更新时间:2023-10-16

根据cachegrind,此校验和计算例程是整个应用程序中指令缓存加载和指令缓存未命中的最大贡献者之一:

#include <stdint.h>
namespace {
uint32_t OnesComplementSum(const uint16_t * b16, int len)  {
    uint32_t sum = 0;
    uint32_t a = 0;
    uint32_t b = 0;
    uint32_t c = 0;
    uint32_t d = 0;
    // helper for the loop unrolling
    auto run8 = [&] {
        a += b16[0];
        b += b16[1];
        c += b16[2];
        d += b16[3];
        b16 += 4;
    };
    for (;;) {
        if (len > 32) {
            run8();
            run8();
            run8();
            run8();
            len -= 32;
            continue;
        }
        if (len > 8) {
            run8();
            len -= 8;
            continue;
        }
        break;
    }
    sum += (a + b) + (c + d);
    auto reduce = [&]() {
        sum = (sum & 0xFFFF) + (sum >> 16);
        if (sum > 0xFFFF) sum -= 0xFFFF;
    };
    reduce();
    while ((len -= 2) >= 0) sum += *b16++;
    if (len == -1) sum += *(const uint8_t *)b16; // add the last byte
    reduce();
    return sum;
}    
} // anonymous namespace     
uint32_t get(const uint16_t* data, int length)
{
    return OnesComplementSum(data, length);
}

请参阅此处的asm输出。

也许它是由循环展开引起的,但生成的对象代码似乎并不太多。

如何改进代码?

更新

  • 因为校验和函数位于匿名命名空间中,所以它被位于同一cpp文件中的两个函数内联并重复
  • 循环展开仍然是有益的。删除它会减慢代码的速度
  • 改进无限循环加快了代码的速度(但由于某种原因,我在mac上得到了相反的结果)。

    • 修复之前:在这里你可以看到两个校验和和17210个L1 IR未命中
    • 修复后:修复内联问题和无限循环后,L1指令缓存未命中率降至8324
    • "InstructionFetch"在固定示例中较高。我不知道该怎么解释。这只是意味着大多数活动都发生在那里吗?还是暗示了一个问题

将主循环替换为:

const int quick_len=len/8;
const uint16_t * const the_end=b16+quick_len*4;
len -= quick_len*8;
for (; b16+4 <= the_end; b16+=4)
{
    a += b16[0];
    b += b16[1];
    c += b16[2];
    d += b16[3];
}

如果你使用-O3,似乎没有必要手动循环展开

此外,测试用例允许过多的优化,因为输入是静态的,结果是未使用的,打印出结果也有助于验证优化版本不会破坏任何

我使用的完整测试:

int main(int argc, char *argv[])
{
    using namespace std::chrono;
    auto start_time = steady_clock::now();
    int ret=OnesComplementSum((const uint8_t*)(s.data()+argc), s.size()-argc, 0);
    auto elapsed_ns = duration_cast<nanoseconds>(steady_clock::now() - start_time).count();
    std::cout << "loop=" << loop << " elapsed_ns=" << elapsed_ns << " = " << ret<< std::endl;
    return ret;
}

与theis(CLEAN LOOP)和您的改进版本(UGLY LOOP)以及更长的测试字符串的比较

loop=CLEAN_LOOP  elapsed_ns=8365  =  14031
loop=CLEAN_LOOP  elapsed_ns=5793  =  14031
loop=CLEAN_LOOP  elapsed_ns=5623  =  14031
loop=CLEAN_LOOP  elapsed_ns=5585  =  14031
loop=UGLY_LOOP   elapsed_ns=9365  =  14031
loop=UGLY_LOOP   elapsed_ns=8957  =  14031
loop=UGLY_LOOP   elapsed_ns=8877  =  14031
loop=UGLY_LOOP   elapsed_ns=8873  =  14031

此处验证:http://coliru.stacked-crooked.com/a/52d670039de17943

编辑:

事实上,整个功能可以简化为:

uint32_t OnesComplementSum(const uint8_t* inData, int len, uint32_t sum)
{
  const uint16_t * b16 = reinterpret_cast<const uint16_t *>(inData);
  const uint16_t * const the_end=b16+len/2;
  for (; b16 < the_end; ++b16)
  {
    sum += *b16;
  }
  sum = (sum & uint16_t(-1)) + (sum >> 16);
  return (sum > uint16_t(-1)) ? sum - uint16_t(-1) : sum;
}

哪个比-O3的OP更好,但与-O2:相比更差

http://coliru.stacked-crooked.com/a/bcca1e94c2f394c7

loop=CLEAN_LOOP  elapsed_ns=5825  =  14031
loop=CLEAN_LOOP  elapsed_ns=5717  =  14031
loop=CLEAN_LOOP  elapsed_ns=5681  =  14031
loop=CLEAN_LOOP  elapsed_ns=5646  =  14031
loop=UGLY_LOOP   elapsed_ns=9201  =  14031
loop=UGLY_LOOP   elapsed_ns=8826  =  14031
loop=UGLY_LOOP   elapsed_ns=8859  =  14031
loop=UGLY_LOOP   elapsed_ns=9582  =  14031

所以里程数可能会有所不同,除非知道确切的体系结构,否则我只会选择更简单的