gez*_*eza 5 performance benchmarking x86 x86-64 inline-assembly
在答案中,我已经声明未对齐访问的速度与对齐访问的速度几乎相同(在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);
}
}
Run Code Online (Sandbox Code Playgroud)
Pet*_*des 15
时间方法.我可能会设置它,所以测试是由命令行arg选择的,所以我可以计时perf stat ./unaligned-test,并获得perf计数器结果而不是每次测试的挂钟时间.这样,我就不必关心涡轮/节能,因为我可以测量核心时钟周期.(除非您禁用turbo和其他频率变化,否则与gettimeofday/ rdtscreference周期不同.)
您只测试吞吐量,而不是延迟,因为没有任何负载依赖.
您的缓存数量将比您的内存数量更差,但您可能不会意识到这是因为您的缓存数量可能是由于处理跨越缓存行边界的加载/存储的拆分加载寄存器数量的瓶颈.对于顺序读取,高速缓存的外部级别仍然只是看到整个高速缓存行的一系列请求.只有从L1D获取数据的执行单元才需要关注对齐.要测试非缓存情况的未对齐,您可以执行分散加载,因此缓存行拆分需要将两个缓存行带入L1.
高速缓存行是64B宽1,因此您总是测试高速缓存行拆分和高速缓存行访问的混合.测试总是分裂的负载会对分裂负载的微架构资源造成更大的困难.(实际上,根据您的CPU,缓存提取宽度可能比线路大小更窄.最近的Intel CPU可以从缓存线内部获取任何未对齐的块,但这是因为它们具有特殊的硬件来实现这一点.其他CPU可能只有在自然对齐的16B块或其他内容中获取时才会最快. @ BobOnRope说AMD CPU可能关心16B和32B边界.)
你根本不测试store-> load forwarding.对于现有的测试,以及一种可视化不同路线结果的好方法,请参阅这篇stuffedcow.net博客文章:x86处理器中的存储到转发转发和内存消歧.
通过内存传递数据是一个重要的用例,错位+缓存行拆分可能会干扰某些CPU上的存储转发.要正确测试,请确保测试不同的错位,而不仅仅是1:15(向量)或1:3(整数).(您目前仅测试相对于16B对齐的+1偏移).
我忘记了它是仅用于存储转发还是用于常规加载,但是当负载在缓存线边界上均匀分割时(8:8向量,也可能是4:4或2:2)可能会减少惩罚整数分裂).你应该测试一下.(我可能会想到P4 lddqu或Core 2 movqdu)
英特尔的优化手册中包含大量未对齐的表格,而不是从广泛的商店转发到完全包含在其中的狭窄重新加载.在某些CPU上,当宽存储自然对齐时,即使它不跨越任何缓存行边界,这在更多情况下也适用.(也许在SnB/IvB上,因为他们使用带有16B库的库存L1缓存,并且拆分它们会影响商店转发.我没有重新检查手册,但如果你真的想通过实验测试,那就是你的东西应该寻找.)
这提醒我,未对齐的负载更有可能在SnB/IvB上引发缓存库冲突(因为一个负载可以触及两个存储区).但是你不会从单个流中看到这种加载,因为在一个周期内两次访问同一行中的同一个库是可以的.它只能访问不同行中的同一个库,而这些行不能在同一个周期中发生.(例如,当两次存储器访问是128B的倍数时.)
您不会尝试测试4k页面拆分.它们比常规缓存行拆分慢,因为它们还需要两次TLB检查.(Skylake将它们从大约100周期的惩罚提高到超过正常负载使用延迟的~5周期惩罚,但是)
您无法movups在对齐的地址上进行测试,因此即使内存在运行时对齐,您也不会检测到movups比movapsCore2及更早的更慢.(我认为mov即使在Core2中,最多8个字节的未对齐加载也没问题,只要它们没有跨越缓存行边界.IDK你需要查看多长时间才能找到非向量加载的问题在一个缓存行中.它只是一个32位的CPU,但你仍然可以用MMX或SSE甚至x87测试8B负载.P5奔腾以及后来保证对齐的8B加载/存储是原子的,但是P6和更新的保证只要不跨越缓存行边界,缓存的8B加载/存储就是原子的.与AMD不同,即使在可缓存的内存中,8B边界对于原子性保证很重要. 为什么在x86上对自然对齐的变量进行整数赋值?)
去看看Agner Fog的东西,了解更多关于未对齐载荷如何变慢的信息,并做一些测试来练习这些案例.实际上,Agner可能不是最好的资源,因为他的微观指南主要集中在通过管道获取uops.只是简要提一下缓存行拆分的成本,没有深入了解吞吐量与延迟.
另请参阅:来自Dark Shikari的博客(x264首席开发人员)的Cacheline拆分,取两个,讨论Core2上的未对齐加载策略:检查对齐并为块使用不同的策略是值得的.
脚注:
另见Skylake的uarch-bench结果.显然有人已经编写了一个测试程序,可以检查相对于缓存行边界的每个可能的错位.
寻址模式会影响负载使用延迟,与优化手册中的英特尔文档完全相同.我测试了整数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
Run Code Online (Sandbox Code Playgroud)
然后运行
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% )
Run Code Online (Sandbox Code Playgroud)
在这种情况下,我正在测试mov rax, [rax],自然对齐,所以cycle = 4*L1-dcache-loads.4c延迟.我没有禁用turbo或类似的东西.由于核心没有任何东西,核心时钟周期是最好的衡量方法.
[base + 0..2047]:4c负载使用延迟,11c缓存行拆分,11c 4k页拆分(即使在同一个巨页内).当基数+偏移量与基数不同时,请参阅是否存在惩罚?有关详细信息:如果base+disp结果与其他页面不同base,则必须重播加载uop.[rax - 16].这不是disp8与disp32的区别.所以:hugepages无助于避免页面拆分惩罚(至少在TLB中两个页面都很热时都不会).高速缓存行拆分使寻址模式无关紧要,但"快速"寻址模式对正常和页面拆分负载的延迟降低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= 2x的数量mov rdi, [rdi],在另一个测试中使用基本相同的循环.(这是一个依赖负载情况,而不是吞吐量限制.)在AGU之前,您无法检测到分割负载.
据推测,当加载uop发现它需要来自第二行的数据时,它会查找拆分寄存器(Intel CPU用来处理拆分加载的缓冲区),并将第一行所需的数据部分放入该拆分中REG.并且还向RS发回需要重播的信号.(这是猜测.)
另请参阅IvyBridge上指针追逐循环中附近相关商店的奇怪性能影响.添加额外的负载会加快速度吗?有关uop重播的更多信息.(但请注意,uops 依赖于加载,而不是加载uop本身.我认为缓存未命中加载会设置所有内容,以便在数据到达时使用数据,而不必自行重放.问题是调度程序主动调度当负载数据可能从L2缓存到达时,uops消耗数据以在循环中调度,而不是等待一个额外的周期来查看它是否有.在那个Q&A中,依赖的uops也主要是负载.)
因此,我认为即使不存在高速缓存行,分割负载重放也应该在几个周期内发生,因此分割两侧的需求加载请求可以立即进行.
SKL有两个硬件页面遍历单元,这可能与4k分割性能的大幅提升有关.即使没有TLB未命中,可能是较旧的CPU必须考虑到可能存在的事实.
有趣的是,4k-split吞吐量是非整数的.我认为我的测量结果具有足够的精度和可重复性.请记住,每次加载都是4k-split,没有其他工作正在进行(除了在一个小的dec/jnz循环中).如果你有真正的代码,你做的事情真的错了.
我没有任何可靠的猜测,为什么它可能是非整数,但显然有很多必须在微架构上发生4k-split.它仍然是缓存行拆分,它必须检查TLB两次.
| 归档时间: |
|
| 查看次数: |
881 次 |
| 最近记录: |