如何准确地对x86_64的未对齐访问速度进行基准测试



在回答中,我已经说过未对齐访问与长时间对齐访问的速度几乎相同(在x86/x86_64上)。我没有任何数字来支持这个说法,所以我为它创建了一个基准。

你认为这个基准有什么缺陷吗?你能改进它吗(我的意思是,增加 GB/秒,以便更好地反映事实)?

#include <sys/time.h>
#include <stdio.h>
template <int N>
__attribute__((noinline))
void loop32(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("mov     (%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x04(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x08(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x0c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x10(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x14(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x18(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x1c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x20(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x24(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x28(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x2c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x30(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x34(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x38(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x3c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x40(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x44(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x48(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x4c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x50(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x54(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x58(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x5c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x60(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x64(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x68(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x6c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x70(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x74(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x78(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x7c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x80(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x84(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x88(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x8c(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x90(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x94(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x98(%0), %%eax" : : "r"(v) :"eax");
__asm__ ("mov 0x9c(%0), %%eax" : : "r"(v) :"eax");
v += 160;
}
}
template <int N>
__attribute__((noinline))
void loop64(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("mov     (%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x08(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x10(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x18(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x20(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x28(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x30(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x38(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x40(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x48(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x50(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x58(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x60(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x68(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x70(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x78(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x80(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x88(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x90(%0), %%rax" : : "r"(v) :"rax");
__asm__ ("mov 0x98(%0), %%rax" : : "r"(v) :"rax");
v += 160;
}
}
template <int N>
__attribute__((noinline))
void loop128a(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("movaps     (%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x10(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x20(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x30(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x40(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x50(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x60(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x70(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x80(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movaps 0x90(%0), %%xmm0" : : "r"(v) :"xmm0");
v += 160;
}
}
template <int N>
__attribute__((noinline))
void loop128u(const char *v) {
for (int i=0; i<N; i+=160) {
__asm__ ("movups     (%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x10(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x20(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x30(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x40(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x50(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x60(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x70(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x80(%0), %%xmm0" : : "r"(v) :"xmm0");
__asm__ ("movups 0x90(%0), %%xmm0" : : "r"(v) :"xmm0");
v += 160;
}
}
long long int t() {
struct timeval tv;
gettimeofday(&tv, 0);
return (long long int)tv.tv_sec*1000000 + tv.tv_usec;
}
int main() {
const int ITER = 10;
const int N = 1600000000;
char *data = reinterpret_cast<char *>(((reinterpret_cast<unsigned long long>(new char[N+32])+15)&~15));
for (int i=0; i<N+16; i++) data[i] = 0;
{
long long int t0 = t();
for (int i=0; i<ITER*100000; i++) {
loop32<N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<ITER*100000; i++) {
loop32<N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<ITER; i++) {
loop32<N>(data);
}
long long int t3 = t();
for (int i=0; i<ITER; i++) {
loop32<N>(data+1);
}
long long int t4 = t();
printf(" 32-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%n", (double)N*ITER/(t1-t0)/1000, (double)N*ITER/(t2-t1)/1000, 100.0*(t2-t1)/(t1-t0)-100.0f);
printf(" 32-bit,   mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%n", (double)N*ITER/(t3-t2)/1000, (double)N*ITER/(t4-t3)/1000, 100.0*(t4-t3)/(t3-t2)-100.0f);
}
{
long long int t0 = t();
for (int i=0; i<ITER*100000; i++) {
loop64<N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<ITER*100000; i++) {
loop64<N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<ITER; i++) {
loop64<N>(data);
}
long long int t3 = t();
for (int i=0; i<ITER; i++) {
loop64<N>(data+1);
}
long long int t4 = t();
printf(" 64-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%n", (double)N*ITER/(t1-t0)/1000, (double)N*ITER/(t2-t1)/1000, 100.0*(t2-t1)/(t1-t0)-100.0f);
printf(" 64-bit,   mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%n", (double)N*ITER/(t3-t2)/1000, (double)N*ITER/(t4-t3)/1000, 100.0*(t4-t3)/(t3-t2)-100.0f);
}
{
long long int t0 = t();
for (int i=0; i<ITER*100000; i++) {
loop128a<N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<ITER*100000; i++) {
loop128u<N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<ITER; i++) {
loop128a<N>(data);
}
long long int t3 = t();
for (int i=0; i<ITER; i++) {
loop128u<N>(data+1);
}
long long int t4 = t();
printf("128-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%n", (double)N*ITER/(t1-t0)/1000, (double)N*ITER/(t2-t1)/1000, 100.0*(t2-t1)/(t1-t0)-100.0f);
printf("128-bit,   mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3f%%n", (double)N*ITER/(t3-t2)/1000, (double)N*ITER/(t4-t3)/1000, 100.0*(t4-t3)/(t3-t2)-100.0f);
}
}

计时方法。 我可能会设置它,以便通过命令行参数选择测试,这样我就可以用perf stat ./unaligned-test来计时,并获得性能计数器结果,而不仅仅是每个测试的挂钟时间。 这样,我就不必关心涡轮增压/省电,因为我可以测量核心时钟周期。 (与gettimeofday/rdtsc参考周期不同,除非您禁用涡轮增压和其他频率变化。


您只是在测试吞吐量,而不是延迟,因为没有任何负载是相关的。

缓存编号将比内存编号差,但您可能不会意识到这是因为缓存编号可能是由于处理跨缓存行边界的加载/存储的拆分加载寄存器数量的瓶颈。 对于顺序读取,缓存的外部级别仍然总是只会看到对整个缓存行的请求序列。 只有从L1D获取数据的执行单元才需要关心对齐。 要测试非缓存情况的未对齐,您可以执行分散加载,因此缓存行拆分需要将两个缓存行引入 L1。

缓存行的宽度为 64 字节1,因此您始终在测试缓存行拆分和缓存行内访问的组合。 测试总是拆分负载会在拆分负载微架构资源上遇到更困难的瓶颈。 (实际上,根据您的 CPU,缓存提取宽度可能比行大小窄。 最近的英特尔CPU可以从缓存行内获取任何未对齐的块,但那是因为它们具有特殊的硬件来使其速度更快。 其他 CPU 可能只有在自然对齐的 16 字节块或其他内容中获取时才能达到最快速度。 @BeeOnRope表示AMD CPU可能关心16字节和32字节边界。

您根本没有测试存储→负载转发。 有关现有测试以及可视化不同对齐方式结果的好方法,请参阅此 stuffedcow.net 博客文章:x86 处理器中的存储到加载转发和内存消除歧义。

通过内存传递数据是一个重要的用例,未对齐 + 缓存行拆分可能会干扰某些 CPU 上的存储转发。 要正确测试这一点,请确保测试不同的错位,而不仅仅是 1:15(矢量)或 1:3(整数)。 (您目前仅测试相对于 16B 对齐的 +1 偏移)。

我忘记了它是否仅用于存储转发或常规加载,但是当负载在缓存行边界上均匀分配时(8:8 向量,也可能是 4:4 或 2:2 整数拆分),损失可能会更少。 你应该测试一下。 (我可能会想到P4lddqu或Core 2movqdu)

英特尔的优化手册有大表格,列出了错位与从宽存储到完全包含在其中的狭窄重新加载的存储转发。 在某些 CPU 上,这在宽存储自然对齐的更多情况下有效,即使它不跨越任何缓存行边界也是如此。 (也许在 SnB/IvB 上,因为它们使用具有 16B 银行的银行 L1 缓存,并且在这些银行之间进行拆分可能会影响存储转发。

我没有重新检查手册,但如果你真的想通过实验来测试它,那是你应该寻找的东西。


这提醒了我,未对齐的负载更有可能在 SnB/IvB 上引发缓存库冲突(因为一个负载可以触及两个库)。 但是您不会看到来自单个流的这种加载,因为在一个周期内两次访问同一行中的同一银行是可以的。 它只是在不同的线路中访问同一家银行,而这在同一周期中不可能发生。 (例如,当两个内存访问相隔 128 字节的倍数时。

您不会尝试测试 4k 分页。 它们比常规缓存行拆分慢,因为它们还需要两次 TLB 检查。 (不过,Skylake将它们从~100个周期的惩罚改进为~5个周期的惩罚,超出了正常的负载使用延迟)

您无法在对齐的地址上测试movups,因此即使内存在运行时对齐,您也不会检测到movups比 Core 2 及更早版本上的movaps慢。 (我认为即使在 Core 8 中,未对齐的mov加载高达 2 字节也可以,只要它们不越过缓存行边界。 IDK 您必须查看的 CPU 年龄才能发现缓存行内非矢量加载的问题。 它将是一个只有 32 位的 CPU,但您仍然可以使用 MMX 或 SSE 甚至 x87 测试 8 字节负载。 P5 奔腾及更高版本保证对齐的 8 字节加载/存储是原子的,但 P6 和更高版本保证缓存的 8 字节加载/存储是原子的,只要不跨越缓存行边界。 与AMD不同,8字节边界对于原子性保证很重要,即使在可缓存内存中也是如此。为什么在 x86 上自然对齐的变量原子上分配整数?

去看看Agner Fog的东西,了解更多关于未对齐的负载如何变慢的信息,并准备测试来练习这些情况。 实际上,Agner 可能不是最好的资源,因为他的微架构指南主要关注通过管道获取 uops。 只是简要提及缓存行拆分的成本,没有深入介绍吞吐量与延迟。

另请参阅:缓存行拆分,取两个,来自 Dark Shikari 的博客(x264 首席开发人员),讨论 Core2 上的未对齐加载策略:检查对齐情况并对块使用不同的策略是值得的。


脚注 1如今,64B 缓存行是一个安全的假设。 奔腾 3 及更早版本有 32B 行。 P4有64B线,但它们通常以128B对齐的对转移。 我想我记得读到P4实际上在L2或L3中有128B线,但也许这只是成对传输的64B线的失真。 7-CPU明确表示P4 130nm的两个缓存级别中都有64B行。

现代英特尔 CPU 具有相邻线 L2"空间"预取,同样倾向于拉入 128 字节对齐对的另一半,这在某些情况下会增加错误共享。 x86-64 的缓存填充大小应该是 128 字节吗?展示了一个证明这一点的实验。


另请参阅Skylake的uarch-bench结果。 显然,有人已经编写了一个测试器,用于检查相对于缓存行边界的每个可能的错位。

<小时 />

我在天湖桌面上的测试 (i7-6700k)

寻址模式会影响负载使用延迟,正如英特尔在其优化手册中记录的那样。 我用整数mov rax, [rax+...]movzx/sx进行了测试(在这种情况下使用加载的值作为索引,因为它太窄而无法成为指针)。

;;;  Linux x86-64 NASM/YASM source.  Assemble into a static binary
;; public domain, originally written by peter@cordes.ca.
;; Share and enjoy.  If it breaks, you get to keep both pieces.
;;; This kind of grew while I was testing and thinking of things to test
;;; I left in some of the comments, but took out most of them and summarized the results outside this code block
;;; When I thought of something new to test, I'd edit, save, and up-arrow my assemble-and-run shell command
;;; Then edit the result into a comment in the source.
section .bss
ALIGN   2 * 1<<20   ; 2MB = 4096*512.  Uses hugepages in .bss but not in .data.  I checked in /proc/<pid>/smaps
buf:    resb 16 * 1<<20
section .text
global _start
_start:
mov     esi, 128
;   mov             edx, 64*123 + 8
;   mov             edx, 64*123 + 0
;   mov             edx, 64*64 + 0
xor             edx,edx
;; RAX points into buf, 16B into the last 4k page of a 2M hugepage
mov             eax, buf + (2<<20)*0 + 4096*511 + 64*0 + 16
mov             ecx, 25000000
%define ADDR(x)  x                     ; SKL: 4c
;%define ADDR(x)  x + rdx              ; SKL: 5c
;%define ADDR(x)  128+60 + x + rdx*2   ; SKL: 11c cache-line split
;%define ADDR(x)  x-8                 ; SKL: 5c
;%define ADDR(x)  x-7                 ; SKL: 12c for 4k-split (even if it's in the middle of a hugepage)
; ... many more things and a block of other result-recording comments taken out
%define dst rax

mov             [ADDR(rax)], dst
align 32
.loop:
mov             dst, [ADDR(rax)]
mov             dst, [ADDR(rax)]
mov             dst, [ADDR(rax)]
mov             dst, [ADDR(rax)]
dec         ecx
jnz .loop
xor edi,edi
mov eax,231
syscall

然后运行

asm-link load-use-latency.asm && disas load-use-latency && 
perf stat -etask-clock,cycles,L1-dcache-loads,instructions,branches -r4 ./load-use-latency
+ yasm -felf64 -Worphan-labels -gdwarf2 load-use-latency.asm
+ ld -o load-use-latency load-use-latency.o
(disassembly output so my terminal history has the asm with the perf results)
Performance counter stats for './load-use-latency' (4 runs):
91.422838      task-clock:u (msec)       #    0.990 CPUs utilized            ( +-  0.09% )
400,105,802      cycles:u                  #    4.376 GHz                      ( +-  0.00% )
100,000,013      L1-dcache-loads:u         # 1093.819 M/sec                    ( +-  0.00% )
150,000,039      instructions:u            #    0.37  insn per cycle           ( +-  0.00% )
25,000,031      branches:u                #  273.455 M/sec                    ( +-  0.00% )
0.092365514 seconds time elapsed                                          ( +-  0.52% )

在这种情况下,我正在测试mov rax, [rax],自然对齐,因此周期= 4 * L1-dcache加载。 4c 延迟。 我没有禁用涡轮增压或类似的东西。 由于内核上没有任何东西离开,因此内核时钟周期是最好的测量方法。

  • [base + 0..2047]:4c 加载使用延迟、11c 缓存行拆分、11c 4k 页面拆分(即使在同一个大页面内)。 请参阅当基数+偏移量与基数位于不同的页面中时是否会受到惩罚?有关更多详细信息:如果base+dispbase位于不同的页面中,则必须重播加载UOP。
  • 任何其他寻址模式:5C 延迟、11C 缓存行拆分、12C 4K 拆分(即使在 HugePage 中)。 这包括[rax - 16]. 区别不在于 disp8 与 disp32

所以:巨大的页面无助于避免页面拆分处罚(至少当两个页面在TLB中都很热时)。 缓存行拆分使寻址模式无关紧要,但"快速"寻址模式对于正常和页面拆分加载的延迟降低了 1c。

4k 拆分处理比以前好得多,请参阅@harold的数字,其中 Haswell 对 4k 拆分有 ~32c 延迟。 (较旧的CPU可能比这更糟糕。 我认为在 SKL 之前应该是 ~100 个周期的惩罚。

吞吐量(无论寻址模式如何),通过使用rax以外的目标进行测量,因此负载是独立的:

  • 无分离:0.5C。
  • CL 分裂:1c。
  • 4k-split:~3.8到3.9c(比Skylake之前的CPU好得多)

与预期相同的movzx/movsx吞吐量/延迟(包括 WORD 拆分),因为它们在加载端口中处理(与某些 AMD CPU 不同,其中还有一个 ALU uop)。

依赖于缓存行拆分负载的 Uops 将从 RS(预留站)重放。uops_dispatched_port.port_2+port_3= 2xmov rdi, [rdi]数的计数器,在另一个使用基本相同循环的测试中。 (这是一个依赖负载的情况,不受吞吐量限制。 在 AGU 生成线性地址之前,CPU 无法检测到拆分负载。

我以前认为拆分负载本身会重放,但这是基于这个指针跟踪测试,其中每个负载都依赖于以前的负载。 如果我们在循环中放置一个imul rdi, rdi, 1,我们将获得额外的端口 1 ALU 计数,因为它被重放,而不是负载。

拆分加载只需要调度一次,但我不确定它后来是否在同一加载端口中借用一个周期来访问另一个缓存行(并将其与保存在该加载端口内的拆分寄存器中的第一部分合并。 或者,如果另一条线路在 L1d 中不存在,则启动该线路的需求负载。

无论细节如何,即使避免重播负载,缓存行拆分加载的吞吐量也低于非拆分负载。 (无论如何,我们没有用它测试指针追逐。

另请参阅来自附近依赖商店的奇怪性能效果,在 IvyBridge 上的指针追逐循环中。增加额外的负载可以加快速度?有关 UOP 重播的更多信息。 (但请注意,这是针对依赖于负载uop,而不是负载 uop 本身。 在该问答中,依赖的uop也大多是负载。

缓存未命中加载本身不需要重放以在准备就绪时"接受"传入数据,只需依赖 uops。 请参阅聊天讨论,了解负载操作在调度、完成或其他时间是否从 RS 中解除分配?。 i7-6700k 上的此 https://godbolt.org/z/HJF3BN NASM 测试用例显示,无论 L1d 命中还是 L3 命中,调度的负载 uops 数量都相同。 但是调度的 ALU uop 数量(不包括环路开销)从每个负载 1 个增加到每个负载 ~8.75 个。 调度程序积极地调度使用数据的 uops 在加载数据可能从 L2 缓存到达的周期中调度(然后似乎非常积极地),而不是等待一个额外的周期来查看它是否这样做。

我们还没有测试过当有其他独立但更年轻的工作可以在同一端口上完成时,重放有多激进,这些端口的输入肯定是准备好的。


SKL 有两个硬件页面行走单元,这可能与 4k 拆分性能的巨大改进有关。 即使没有TLB未命中,可能较旧的CPU也必须考虑到可能存在的事实。

有趣的是,4k 拆分吞吐量是非整数的。 我认为我的测量具有足够的精度和可重复性来说明这一点。 请记住,这是每个负载都是4k 拆分,并且没有其他工作正在进行(除了在一个小的 dec/jnz 循环内)。 如果你在实际代码中有这个,你就做错了什么。

我对为什么它可能是非整数没有任何可靠的猜测,但显然,对于 4k 拆分,在微观架构上必须发生很多事情。 它仍然是缓存行拆分,并且必须检查TLB两次。

测试各种偏移量的 64 位负载(下面的代码),我在 Haswell 上的原始结果是:

aligned L: 4.01115 T: 0.500003
ofs1 L: 4.00919 T: 0.500003
ofs2 L: 4.01494 T: 0.500003
ofs3 L: 4.01403 T: 0.500003
ofs7 L: 4.01073 T: 0.500003
ofs15 L: 4.01937 T: 0.500003
ofs31 L: 4.02107 T: 0.500002
ofs60 L: 9.01482 T: 1
ofs62 L: 9.03644 T: 1
ofs4092 L: 32.3014 T: 31.1967

根据需要应用舍入。它们中的大多数显然应该向下舍入,但 .3 和 .2(来自页面边界交叉)可能太重要而不能成为噪音。这只测试了具有简单地址的负载,并且只测试了"纯负载",没有转发。

我的结论是,缓存行内的对齐与标量加载无关,只有跨越缓存行边界和(特别是出于显而易见的原因)跨越页面边界很重要。在这种情况下,在中间或其他地方越过缓存行边界似乎没有区别。

AMD偶尔会对16字节边界产生一些有趣的效果,但我无法测试。

以下是原始(!) xmm矢量结果,其中包括pextrq的影响,因此减去两个延迟周期:

aligned L: 8.05247 T: 0.500003
ofs1 L: 8.03223 T: 0.500003
ofs2 L: 8.02899 T: 0.500003
ofs3 L: 8.05598 T: 0.500003
ofs7 L: 8.03579 T: 0.500002
ofs15 L: 8.02787 T: 0.500003
ofs31 L: 8.05002 T: 0.500003
ofs58 L: 13.0404 T: 1
ofs60 L: 13.0825 T: 1
ofs62 L: 13.0935 T: 1
ofs4092 L: 36.345 T: 31.2357

测试代码是

global test_unaligned_l
proc_frame test_unaligned_l
alloc_stack 8
[endprolog]
mov r9, rcx
rdtscp
mov r8d, eax
mov ecx, -10000000
mov rdx, r9
.loop:
mov rdx, [rdx]
mov rdx, [rdx]
add ecx, 1
jnc .loop
rdtscp
sub eax, r8d
add rsp, 8
ret
endproc_frame
global test_unaligned_tp
proc_frame test_unaligned_tp
alloc_stack 8
[endprolog]
mov r9, rcx
rdtscp
mov r8d, eax
mov ecx, -10000000
mov rdx, r9
.loop:
mov rax, [rdx]
mov rax, [rdx]
add ecx, 1
jnc .loop
rdtscp
sub eax, r8d
add rsp, 8
ret
endproc_frame

对于大致相似的向量,但在延迟测试中pextrq

使用以各种偏移量准备的一些数据,例如:

align 64
%rep 31
db 0
%endrep
unaligned31: dq unaligned31
align 4096
%rep 60
db 0
%endrep
unaligned60: dq unaligned60
align 4096
%rep 4092
db 0
%endrep
unaligned4092: dq unaligned4092

为了更多地关注新标题,我将描述它试图做什么以及为什么。

首先,有一个延迟测试。从某个不在eax中的指针将一百万个东西加载到eax中(就像问题中的代码一样)测试吞吐量,这只是图片的一半。对于微不足道的标量载荷,对于矢量载荷,我使用了成对:

movdqu xmm0, [rdx]
pextrq rdx, xmm0, 0

pextrq的延迟为 2,这就是为什么矢量负载的延迟数字都太高了 2,如前所述。

为了便于执行此延迟测试,数据是一个自引用指针。这是一个相当不典型的场景,但它不应该影响负载的时序特性。

吞吐量测试每个循环有两个负载,而不是一个负载,以避免受到循环开销的瓶颈。可以使用更多的负载,但这在 Haswell 上不是必需的(或者我能想到的任何东西,但理论上可能存在具有较低分支吞吐量或较高负载吞吐量的微架构)。

我对 TSC 读取中的屏蔽或补偿其开销(或其他开销)不是很小心。我也没有禁用 Turbo,我只是让它以涡轮频率运行并除以 TSC 速率和涡轮频率之间的比率,这可能会对时间产生一些影响。与 1E7 量级的基准相比,所有这些影响都很小,无论如何结果都可以四舍五入。

所有时间都是 30 局两胜,在这些微观基准上,平均值和方差等内容毫无意义,因为基本事实不是一个随机过程,其中包含我们想要估计的参数,而是一些固定的整数1(或分数的整数倍,用于吞吐量)。几乎所有的噪声都是正的,除了(相对理论的)基准指令在第一次TSC读取之前"泄漏"的情况(如有必要甚至可以避免),因此采取最小值是合适的。

注1:除了显然越过4k边界外,那里正在发生一些奇怪的事情。

我把我改进的一点基准放在这里。仍然仅测量吞吐量(并且仅测量未对齐的偏移量 1)。根据其他答案,我添加了测量 64 字节和 4096 字节拆分。

对于 4k 分割,有很大的不同!但是,如果数据没有越过 64 字节边界,则根本没有速度损失(至少对于我测试过的这 2 个处理器)。

看看这些数字(以及其他答案的数字),我的结论是,未对齐的访问平均速度很快(吞吐量和延迟),但在某些情况下可能会慢得多。但这并不意味着不鼓励使用它们。

我的基准测试产生的原始数字应该持保留态度(正确编写的asm代码很可能优于它),但这些结果大多与Harold对Haswell的回答一致(差异列)。

Haswell:
Full:
32-bit, cache: aligned:  33.2901 GB/sec unaligned:  29.5063 GB/sec, difference: 1.128x
32-bit,   mem: aligned:  12.1597 GB/sec unaligned:  12.0659 GB/sec, difference: 1.008x
64-bit, cache: aligned:  66.0368 GB/sec unaligned:  52.8914 GB/sec, difference: 1.249x
64-bit,   mem: aligned:  16.1317 GB/sec unaligned:  16.0568 GB/sec, difference: 1.005x
128-bit, cache: aligned: 129.8730 GB/sec unaligned:  87.9791 GB/sec, difference: 1.476x
128-bit,   mem: aligned:  16.8150 GB/sec unaligned:  16.8151 GB/sec, difference: 1.000x
JustBoundary64:
32-bit, cache: aligned:  32.5555 GB/sec unaligned:  16.0175 GB/sec, difference: 2.032x
32-bit,   mem: aligned:   1.0044 GB/sec unaligned:   1.0001 GB/sec, difference: 1.004x
64-bit, cache: aligned:  65.2707 GB/sec unaligned:  32.0431 GB/sec, difference: 2.037x
64-bit,   mem: aligned:   2.0093 GB/sec unaligned:   2.0007 GB/sec, difference: 1.004x
128-bit, cache: aligned: 130.6789 GB/sec unaligned:  64.0851 GB/sec, difference: 2.039x
128-bit,   mem: aligned:   4.0180 GB/sec unaligned:   3.9994 GB/sec, difference: 1.005x
WithoutBoundary64:
32-bit, cache: aligned:  33.2911 GB/sec unaligned:  33.2916 GB/sec, difference: 1.000x
32-bit,   mem: aligned:  11.6156 GB/sec unaligned:  11.6223 GB/sec, difference: 0.999x
64-bit, cache: aligned:  65.9117 GB/sec unaligned:  65.9548 GB/sec, difference: 0.999x
64-bit,   mem: aligned:  14.3200 GB/sec unaligned:  14.3027 GB/sec, difference: 1.001x
128-bit, cache: aligned: 128.2605 GB/sec unaligned: 128.3342 GB/sec, difference: 0.999x
128-bit,   mem: aligned:  12.6352 GB/sec unaligned:  12.6218 GB/sec, difference: 1.001x
JustBoundary4096:
32-bit, cache: aligned:  33.5500 GB/sec unaligned:   0.5415 GB/sec, difference: 61.953x
32-bit,   mem: aligned:   0.4527 GB/sec unaligned:   0.0431 GB/sec, difference: 10.515x
64-bit, cache: aligned:  67.1141 GB/sec unaligned:   1.0836 GB/sec, difference: 61.937x
64-bit,   mem: aligned:   0.9112 GB/sec unaligned:   0.0861 GB/sec, difference: 10.582x
128-bit, cache: aligned: 134.2000 GB/sec unaligned:   2.1668 GB/sec, difference: 61.936x
128-bit,   mem: aligned:   1.8165 GB/sec unaligned:   0.1700 GB/sec, difference: 10.687x
Sandy Bridge (processor from 2011)
Full:
32-bit, cache: aligned:  30.0302 GB/sec unaligned:  26.2587 GB/sec, difference: 1.144x
32-bit,   mem: aligned:  11.0317 GB/sec unaligned:  10.9358 GB/sec, difference: 1.009x
64-bit, cache: aligned:  59.2220 GB/sec unaligned:  41.5515 GB/sec, difference: 1.425x
64-bit,   mem: aligned:  14.5985 GB/sec unaligned:  14.3760 GB/sec, difference: 1.015x
128-bit, cache: aligned: 115.7643 GB/sec unaligned:  45.0905 GB/sec, difference: 2.567x
128-bit,   mem: aligned:  14.8561 GB/sec unaligned:  14.8220 GB/sec, difference: 1.002x
JustBoundary64:
32-bit, cache: aligned:  15.2127 GB/sec unaligned:   3.1037 GB/sec, difference: 4.902x
32-bit,   mem: aligned:   0.9870 GB/sec unaligned:   0.6110 GB/sec, difference: 1.615x
64-bit, cache: aligned:  30.2074 GB/sec unaligned:   6.2258 GB/sec, difference: 4.852x
64-bit,   mem: aligned:   1.9739 GB/sec unaligned:   1.2194 GB/sec, difference: 1.619x
128-bit, cache: aligned:  60.7265 GB/sec unaligned:  12.4007 GB/sec, difference: 4.897x
128-bit,   mem: aligned:   3.9443 GB/sec unaligned:   2.4460 GB/sec, difference: 1.613x
WithoutBoundary64:
32-bit, cache: aligned:  30.0348 GB/sec unaligned:  29.9801 GB/sec, difference: 1.002x
32-bit,   mem: aligned:  10.7067 GB/sec unaligned:  10.6755 GB/sec, difference: 1.003x
64-bit, cache: aligned:  59.1895 GB/sec unaligned:  59.1925 GB/sec, difference: 1.000x
64-bit,   mem: aligned:  12.9404 GB/sec unaligned:  12.9307 GB/sec, difference: 1.001x
128-bit, cache: aligned: 116.4629 GB/sec unaligned: 116.0778 GB/sec, difference: 1.003x
128-bit,   mem: aligned:  11.2963 GB/sec unaligned:  11.3533 GB/sec, difference: 0.995x
JustBoundary4096:
32-bit, cache: aligned:  30.2457 GB/sec unaligned:   0.5626 GB/sec, difference: 53.760x
32-bit,   mem: aligned:   0.4055 GB/sec unaligned:   0.0275 GB/sec, difference: 14.726x
64-bit, cache: aligned:  60.6175 GB/sec unaligned:   1.1257 GB/sec, difference: 53.851x
64-bit,   mem: aligned:   0.8150 GB/sec unaligned:   0.0551 GB/sec, difference: 14.798x
128-bit, cache: aligned: 121.2121 GB/sec unaligned:   2.2455 GB/sec, difference: 53.979x
128-bit,   mem: aligned:   1.6255 GB/sec unaligned:   0.1103 GB/sec, difference: 14.744x

代码如下:

#include <sys/time.h>
#include <stdio.h>
__attribute__((always_inline))
void load32(const char *v) {
__asm__ ("mov     %0, %%eax" : : "m"(*v) :"eax");
}
__attribute__((always_inline))
void load64(const char *v) {
__asm__ ("mov     %0, %%rax" : : "m"(*v) :"rax");
}
__attribute__((always_inline))
void load128a(const char *v) {
__asm__ ("movaps     %0, %%xmm0" : : "m"(*v) :"xmm0");
}
__attribute__((always_inline))
void load128u(const char *v) {
__asm__ ("movups     %0, %%xmm0" : : "m"(*v) :"xmm0");
}
struct Full {
template <int S>
static float factor() {
return 1.0f;
}
template <void (*LOAD)(const char *), int S, int N>
static void loop(const char *v) {
for (int i=0; i<N; i+=S*16) {
LOAD(v+S* 0);
LOAD(v+S* 1);
LOAD(v+S* 2);
LOAD(v+S* 3);
LOAD(v+S* 4);
LOAD(v+S* 5);
LOAD(v+S* 6);
LOAD(v+S* 7);
LOAD(v+S* 8);
LOAD(v+S* 9);
LOAD(v+S*10);
LOAD(v+S*11);
LOAD(v+S*12);
LOAD(v+S*13);
LOAD(v+S*14);
LOAD(v+S*15);
v += S*16;
}
}
};
struct JustBoundary64 {
template <int S>
static float factor() {
return S/64.0f;
}
template <void (*LOAD)(const char *), int S, int N>
static void loop(const char *v) {
static_assert(N%(64*16)==0);
for (int i=0; i<N; i+=64*16) {
LOAD(v+64* 1-S);
LOAD(v+64* 2-S);
LOAD(v+64* 3-S);
LOAD(v+64* 4-S);
LOAD(v+64* 5-S);
LOAD(v+64* 6-S);
LOAD(v+64* 7-S);
LOAD(v+64* 8-S);
LOAD(v+64* 9-S);
LOAD(v+64*10-S);
LOAD(v+64*11-S);
LOAD(v+64*12-S);
LOAD(v+64*13-S);
LOAD(v+64*14-S);
LOAD(v+64*15-S);
LOAD(v+64*16-S);
v += 64*16;
}
}
};
struct WithoutBoundary64 {
template <int S>
static float factor() {
return (64-S)/64.0f;
}
template <void (*LOAD)(const char *), int S, int N>
static void loop(const char *v) {
for (int i=0; i<N; i+=S*16) {
if ((S* 1)&0x3f) LOAD(v+S* 0);
if ((S* 2)&0x3f) LOAD(v+S* 1);
if ((S* 3)&0x3f) LOAD(v+S* 2);
if ((S* 4)&0x3f) LOAD(v+S* 3);
if ((S* 5)&0x3f) LOAD(v+S* 4);
if ((S* 6)&0x3f) LOAD(v+S* 5);
if ((S* 7)&0x3f) LOAD(v+S* 6);
if ((S* 8)&0x3f) LOAD(v+S* 7);
if ((S* 9)&0x3f) LOAD(v+S* 8);
if ((S*10)&0x3f) LOAD(v+S* 9);
if ((S*11)&0x3f) LOAD(v+S*10);
if ((S*12)&0x3f) LOAD(v+S*11);
if ((S*13)&0x3f) LOAD(v+S*12);
if ((S*14)&0x3f) LOAD(v+S*13);
if ((S*15)&0x3f) LOAD(v+S*14);
if ((S*16)&0x3f) LOAD(v+S*15);
v += S*16;
}
}
};
struct JustBoundary4096 {
template <int S>
static float factor() {
return S/4096.0f;
}
template <void (*LOAD)(const char *), int S, int N>
static void loop(const char *v) {
static_assert(N%(4096*4)==0);
for (int i=0; i<N; i+=4096*4) {
LOAD(v+4096*1-S);
LOAD(v+4096*2-S);
LOAD(v+4096*3-S);
LOAD(v+4096*4-S);
v += 4096*4;
}
}
};

long long int t() {
struct timeval tv;
gettimeofday(&tv, 0);
return (long long int)tv.tv_sec*1000000 + tv.tv_usec;
}
template <typename TYPE, void (*LOADa)(const char *), void (*LOADu)(const char *), int S, int N>
void bench(const char *data, int iter, const char *name) {
long long int t0 = t();
for (int i=0; i<iter*100000; i++) {
TYPE::template loop<LOADa, S, N/100000>(data);
}
long long int t1 = t();
for (int i=0; i<iter*100000; i++) {
TYPE::template loop<LOADu, S, N/100000>(data+1);
}
long long int t2 = t();
for (int i=0; i<iter; i++) {
TYPE::template loop<LOADa, S, N>(data);
}
long long int t3 = t();
for (int i=0; i<iter; i++) {
TYPE::template loop<LOADu, S, N>(data+1);
}
long long int t4 = t();
printf("%s-bit, cache: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3fxn", name, (double)N*iter/(t1-t0)/1000*TYPE::template factor<S>(), (double)N*iter/(t2-t1)/1000*TYPE::template factor<S>(), (float)(t2-t1)/(t1-t0));
printf("%s-bit,   mem: aligned: %8.4f GB/sec unaligned: %8.4f GB/sec, difference: %0.3fxn", name, (double)N*iter/(t3-t2)/1000*TYPE::template factor<S>(), (double)N*iter/(t4-t3)/1000*TYPE::template factor<S>(), (float)(t4-t3)/(t3-t2));
}
int main() {
const int ITER = 10;
const int N = 1638400000;
char *data = reinterpret_cast<char *>(((reinterpret_cast<unsigned long long>(new char[N+8192])+4095)&~4095));
for (int i=0; i<N+8192; i++) data[i] = 0;
printf("Full:n");
bench<Full, load32, load32, 4, N>(data, ITER, " 32");
bench<Full, load64, load64, 8, N>(data, ITER, " 64");
bench<Full, load128a, load128u, 16, N>(data, ITER, "128");
printf("nJustBoundary64:n");
bench<JustBoundary64, load32, load32, 4, N>(data, ITER, " 32");
bench<JustBoundary64, load64, load64, 8, N>(data, ITER, " 64");
bench<JustBoundary64, load128a, load128u, 16, N>(data, ITER, "128");
printf("nWithoutBoundary64:n");
bench<WithoutBoundary64, load32, load32, 4, N>(data, ITER, " 32");
bench<WithoutBoundary64, load64, load64, 8, N>(data, ITER, " 64");
bench<WithoutBoundary64, load128a, load128u, 16, N>(data, ITER, "128");
printf("nJustBoundary4096:n");
bench<JustBoundary4096, load32, load32, 4, N>(data, ITER*10, " 32");
bench<JustBoundary4096, load64, load64, 8, N>(data, ITER*10, " 64");
bench<JustBoundary4096, load128a, load128u, 16, N>(data, ITER*10, "128");
}

相关内容

  • 没有找到相关文章

最新更新