在answer中,我已经声明未对齐访问的速度与对齐访问的时间几乎相同(在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);
}
}
答案 0 :(得分:15)
计时方法。我可能会设置它以便测试由命令行arg选择,所以我可以用perf stat ./unaligned-test
计时,并获得perf计数器结果,而不是每次测试的挂钟时间。这样,我不必关心涡轮/节能,因为我可以测量核心时钟周期。 (与gettimeofday
/ rdtsc
参考周期不同,除非您禁用turbo和其他频率变化。)
您只测试吞吐量,而不是延迟,因为没有任何负载是依赖的。
您的缓存数量会比您的内存数字更糟糕,但您可能不会意识到这是因为您的缓存数量可能是因为处理负载的split-load registers数量存在瓶颈问题/跨越缓存行边界的商店。对于顺序读取,高速缓存的外部级别仍然只是看到整个高速缓存行的一系列请求。它只是从L1D获取数据的执行单元必须关心对齐。要测试非缓存情况的错位,可以进行分散加载,因此缓存行拆分需要将两个缓存行放入L1中。
高速缓存行是64B宽 1 ,因此您始终要测试高速缓存行拆分和高速缓存行访问的混合。测试总是分裂的负载会对分裂负载的微架构资源造成更大的困难。 (实际上,取决于您的CPU,cache-fetch width might be narrower than the line size。最近的Intel CPU可以从缓存行中获取任何未对齐的块,但这是因为它们具有特殊的硬件来实现这一点。其他CPU可能只是在自然对齐的16B块或其他内容中获取最快速度。@BeeOnRope says that AMD CPUs may care about 16B and 32B boundaries。)
您根本没有测试 store-&gt;加载转发。对于现有测试,以及一种可视化不同路线结果的好方法,请参阅这篇stuffedcow.net博客文章:Store-to-Load Forwarding and Memory Disambiguation in x86 Processors。
通过内存传递数据是一个重要的用例,错位+缓存行拆分可能会干扰某些CPU上的存储转发。要正确测试,请确保测试不同的错位,而不仅仅是1:15(向量)或1:3(整数)。 (您目前仅测试相对于16B对齐的+1偏移量。)
我忘记了它是仅用于存储转发还是常规加载,但是当负载在缓存线边界上均匀分割时(8:8向量,也可能也是4:4或2:2整数分裂)。你应该测试一下。 (我可能会想到P4 lddqu
或Core 2 movqdu
)
Intel's optimization manual有一个很大的错位表,而不是从广泛的商店转发到完全包含在其中的狭窄重新加载。在某些CPU上,当宽存储自然对齐时,即使它没有跨越任何缓存行边界,这在更多情况下也适用。 (可能在SnB / IvB上,因为它们使用带有16B存储体的存储区L1缓存,并且跨越这些存储区可能会影响存储转发。 我没有重新检查手册,但如果你真的想通过实验测试,那就是你应该寻找的东西。)
这提醒我,未对齐的负载更有可能在SnB / IvB上引发缓存库冲突(因为一个负载可以触及两个存储区)。但是您无法从单个流中看到此加载,因为在一个周期内两次访问相同行中的同一个库很好。它只访问不同行中的同一个银行,这些行不会在同一个周期内发生。 (例如,当两次存储器访问是128B的倍数时。)
您没有尝试测试4k页面拆分。它们比常规缓存行拆分慢,因为它们还需要两次TLB检查。 (Skylake将它们从大约100个周期的惩罚提高到超过正常负载使用延迟的~5个周期惩罚,但是)
您未能在对齐地址上对movups
进行测试,因此您无法检测到movups
在Core2及更早版本上的速度低于movaps
当内存在运行时对齐时。 (我认为,即使在Core2中,未对齐的mov
最多只能加载8个字节,只要它们没有跨越缓存行边界.IDK你需要查看多长时间的CPU在高速缓存行中找到非向量加载的问题。它将是一个仅32位的CPU,但你仍然可以用MMX或SSE甚至x87测试8B加载.P5 Pentium及以后保证对齐的8B加载/存储是原子的,但P6和更新版本保证缓存的8B加载/存储是原子的,只要没有超过缓存行边界。与AMD不同,其中8B边界对于原子性保证很重要,即使在可缓存的内存中也是如此。Why is integer assignment on a naturally aligned variable atomic on x86?)
查看Agner Fog的内容,了解更多关于未对齐的负载如何变慢的信息,以及烹饪测试以执行这些情况。实际上,Agner可能不是最好的资源,因为他的微观指南主要集中在通过管道获取uops。只是简要提一下缓存行拆分的成本,没有深入了解吞吐量与延迟的关系。
另请参阅:来自Dark Shikari的博客(x264首席开发人员)的Cacheline splits, take two,讨论Core2上的未对齐加载策略:检查对齐并为块使用不同的策略是值得的
脚注:
另请参阅uarch-bench结果for Skylake 。显然有人已经编写了一个测试程序,可以检查相对于缓存行边界的每个可能的错位。
寻址模式会影响负载使用延迟,与优化手册中的英特尔文档完全相同。我测试了整数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]
,自然对齐,所以cycles = 4 * L1-dcache-loads。 4c延迟。我没有禁用涡轮增压或类似的东西。由于核心没有任何东西,核心时钟周期是衡量的最佳方式。
[base + 0..2047]
:4c加载使用延迟,11c缓存行拆分,11c 4k页拆分(即使在同一个巨页内)。有关详细信息,请参阅Is there a penalty when base+offset is in a different page than the base?:如果base+disp
的页面与base
不同,则必须重播加载uop。[rax - 16]
。它并不是disp8与disp32之间的差异。所以:largepages不帮助避免页面拆分惩罚(至少在TLB中两个页面都很热时都没有)。缓存行分割使寻址模式无关紧要,但是快速&#34;寻址模式对正常和页面分割负载的延迟降低1c。
4k-split处理比以前更好,请参阅@ harold的数字,其中Haswell的4k-split有~32c延迟。 (而较旧的CPU可能会比那更糟。我认为SKL之前应该是~100周期惩罚。) 吞吐量(无论寻址模式如何),使用rax
以外的目的地来衡量,因此负载是独立的:
movzx/movsx
(包括WORD拆分)的相同吞吐量/延迟,正如预期的那样,因为它们在加载端口中处理(与某些AMD CPU不同,它们也是ALU uop)。
从RS(预订站)重播高速缓存行拆分负载。 uops_dispatched_port.port_2
+ port_3
的计数器= mov rdi, [rdi]
的2x,在使用基本相同的循环的另一个测试中。 (这是一个从属负载情况,而不是吞吐量限制。)在AGU之后,您无法检测到分割负载。
当负载uop发现它需要来自第二行的数据时,它会查找一个拆分寄存器(Intel CPU用来处理拆分负载的缓冲区),并从第一行输入所需的数据部分进入那个分裂注册。并且还向RS发回需要重播的信号。 (这是猜测。)
有关uop重播的更多信息,另请参阅Weird performance effects from nearby dependent stores in a pointer-chasing loop on IvyBridge. Adding an extra load speeds it up?。 (但请注意,uops 依赖于加载,而不是加载uop本身。我认为缓存未命中加载会设置在数据到达时使用数据的所有内容,而不必是重播本身。问题是调度程序主动安排消耗数据的uop在负载数据可能从L2高速缓存到达的周期中调度,而不是等待一个额外的周期来查看它是否有。在那个Q&amp; A中,依赖的uops也主要是负载。)
所以我认为即使两个缓存行都不存在,分割负载重放也应该在几个周期内完成,因此分割两边的需求加载请求可以立即进行。
SKL有两个硬件页面漫游单元,这可能与4k分割性能的大幅提升有关。即使没有TLB未命中,可能是较旧的CPU必须考虑到可能存在的事实。
有趣的是,4k-split吞吐量是非整数的。我认为我的测量结果具有足够的精度和可重复性。请记住,这是每个加载是4k-split,并且没有其他工作正在进行(除了在一个小的dec / jnz循环中)。如果您在实际代码中使用过此代码,那么您确实做错了。
我对于为什么它可能是非整数没有任何可靠的猜测,但显然有很多必须在微架构上发生4k-split。它仍然是一个缓存行拆分,它必须检查TLB两次。
答案 1 :(得分:4)
测试各种偏移的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
的效果,因此减去2个延迟周期:
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,这就是为什么矢量负载的延迟数据都太高了,如上所述。
为了便于进行此延迟测试,数据是一个自引用指针。这是一个相当不典型的情况,但它不应该影响负载的时序特性。
吞吐量测试每个循环有两个负载而不是一个负载,以避免由循环开销造成瓶颈。可以使用更多的负载,但这对Haswell来说是不必要的(或者我能想到的任何东西,但理论上可能存在分支吞吐量较低或负载吞吐量较高的μarch)。
我对TSC读取中的防护或补偿其开销(或其他开销)并不十分谨慎。我也没有禁用Turbo,我只是让它以turbo频率运行并除以TSC速率和turbo频率之间的比率,这可能会影响时序。与大约1E7的基准相比,所有这些效果都很小,无论如何结果都可以舍入。
所有时间都是30的最佳值,平均值和方差等因素在这些微观基准测试中毫无意义,因为基本事实不是一个随机过程,我们想要估算参数,但有些固定整数 [1] (或分数的整数倍,用于吞吐量)。几乎所有的噪声都是正的,除了在第一次TSC读取前基准“泄漏”的(相对理论上的)指令情况(如果有必要,甚至可以避免),所以采取最小值是合适的。
注1:除了明显越过4k边界外,那里发生了一些奇怪的事情。
答案 2 :(得分:1)
我在这里提出了一些改进的基准。仍然仅测量吞吐量(并且仅测量未对齐的偏移1)。基于其他答案,我添加了测量64和4096字节的分割。
对于4k分裂,有一个巨大的差异!但是如果数据没有超过64字节边界,那么根本就没有速度损失(至少对于我测试的这两个处理器而言)。
查看这些数字(以及其他答案的数字),我的结论是未对齐访问平均速度很快(吞吐量和延迟),但有时可能会慢很多。但这并不意味着不鼓励使用它们。
我的基准测试产生的原始数字应该用一粒盐(很可能是正确编写的asm代码优于它),但这些结果大多与哈罗德对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.3fx\n", 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.3fx\n", 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");
}