系统编程( 二 )


// avoid any false sharing between the counters.
// Note: We must use ABSL_CACHELINE_ALIGNED for each member, since we want to
// pad every single counter so it will be forced onto its own separate cache
// line.
struct ABSL_CACHELINE_ALIGNED CacheLineAwareCounters {
ABSL_CACHELINE_ALIGNED std::atomic<int64> success{0};
ABSL_CACHELINE_ALIGNED std::atomic<int64> failure{0};
ABSL_CACHELINE_ALIGNED std::atomic<int64> okay{0};
ABSL_CACHELINE_ALIGNED std::atomic<int64> meh{0};
};这个基准测试分别测试了在运行1个 , 2个,3个和4个线程的情况 。每个线程会触发结构体内一个单独的原子计数器65,536次 。以下是在带有Haswell处理器的2013 MacBook Pro计算机上的处理结果:
Executing tests from //examples:cache-lines
-----------------------------------------------------------------------------
Cache Line Size: 64
sizeof(NormalCounters) = 64
sizeof(CacheLineAwareCounters) = 256
2019-08-13 01:16:18
Run on (4 X 2800 MHz CPU s)
CPU Caches:
L1 Data 32K (x2)
L1 Instruction 32K (x2)
L2 Unified 262K (x2)
L3 Unified 4194K (x1)
---------------------------------------------------------------------------
Benchmark Time CPU Iterations
---------------------------------------------------------------------------
BM_NormalCounters/threads:1 389 us 387 us 1812
BM_NormalCounters/threads:2 1264 us 2517 us 234
BM_NormalCounters/threads:3 1286 us 3718 us 225
BM_NormalCounters/threads:4 1073 us 3660 us 204
BM_CacheLineAwareCounters/threads:1 386 us 385 us 1799
BM_CacheLineAwareCounters/threads:2 200 us 400 us 1658
BM_CacheLineAwareCounters/threads:3 208 us 581 us 1152
BM_CacheLineAwareCounters/threads:4 193 us 721 us 1008
对上述结果作个注释:Time代表每个线程的从开始到结束的挂钟时间(wall clock time),而CPU则代表每个线程使用的CPU时间 。
我们可以看到两个结构体的大小是不同的 , 其中:sizeof(NormalCounters)=64,而 sizeof(CacheLineAwareCounters)=256 。这是因为我们对单个字段施加了对齐约束,这样每个成员都在自己的缓存线上 。因此,它不是像往常的Int64那样占用8个字节,而是占用一个完整的缓存线 , 在我的机器上是64个字节 。
我们还看到对于单线程的情况 , NormalCounters与CacheLineWareCounters的性能差别微乎其微 。但是当我们添加更多线程时,CacheLineAwareCounters的表现要比那些易受“伪共享”错误影响的简单的普通计数器的实现要好得多 。
有趣的是,在单线程的情况下 , CacheLineAwareCounters需要的挂钟时间(wall clock time)比多线程情况下要长 , 这可能指向一些微妙的基准测试问题,或者可能有一个固定的延迟量,但是在多线程时这个延迟量被分散到多个线程中,因此每个线程的延迟量看上去更小了 。
神奇的2的乘方(幂)
在当前的硬件中,除法是最昂贵的操作之一,这里的昂贵意味着“最长延迟” 。Agner Fog的指令延迟列表列出了英特尔公司Skylake处理器的DIV指令在两个64位寄存器上运行,其延迟为35-88个周期,而在相同的两个64位寄存器上运行ADD指令的延迟只有1个周期 。因此,在其它操作能够完成相同工作的地方 , 我们应该尽量避免使用除法操作 。
除了实际做除法外,除法操作常用的一个地方是取模运算(%) 。而取模运算的一个常用的地方是hash表:要从一个hash表转到一个存储桶(bucket),需要进行HASH % TABLE_SIZE这样的取模运算 。取模运算的另一个更加频繁使用的地方是开放寻址算法 , 因为我们需要不断地将值重新映射回hash表存储桶空间 。

相关经验推荐