我发现在MSVC(在Windows上)和GCC(在Linux上)为Ivy Bridge系统编译的代码之间的性能差异很大.代码执行密集矩阵乘法.我使用GCC获得了70%的峰值失误,而MSVC只获得了50%.我想我可能已经把他们两个内在函数如何转换的差异分开了.
__m256 breg0 = _mm256_loadu_ps(&b[8*i])
_mm256_add_ps(_mm256_mul_ps(arge0,breg0), tmp0)
Run Code Online (Sandbox Code Playgroud)
GCC这样做
vmovups ymm9, YMMWORD PTR [rax-256]
vmulps ymm9, ymm0, ymm9
vaddps ymm8, ymm8, ymm9
Run Code Online (Sandbox Code Playgroud)
MSVC这样做
vmulps ymm1, ymm2, YMMWORD PTR [rax-256]
vaddps ymm3, ymm1, ymm3
Run Code Online (Sandbox Code Playgroud)
有人可以向我解释这两种解决方案是否以及为何能够在性能上产生如此大的差异?
尽管MSVC使用少一条指令,但它会将负载与多线程联系起来,这可能会使它更加依赖(也许负载无法按顺序完成)?我的意思是Ivy Bridge可以在一个时钟周期内完成一个AVX加载,一个AVX mult和一个AVX加载,但这要求每个操作都是独立的.
也许问题出在其他地方?您可以在下面看到最里面循环的GCC和MSVC的完整汇编代码.你可以在这里看到循环的C++代码循环展开以实现Ivy Bridge和Haswell的最大吞吐量
g ++ -S -masm = intel matrix.cpp -O3 -mavx -fopenmp
.L4:
vbroadcastss ymm0, DWORD PTR [rcx+rdx*4]
add rdx, 1
add rax, 256
vmovups ymm9, YMMWORD PTR [rax-256]
vmulps ymm9, ymm0, ymm9
vaddps ymm8, ymm8, ymm9
vmovups ymm9, YMMWORD PTR [rax-224] …Run Code Online (Sandbox Code Playgroud) 这与此问题有关,但不一样:x86-64汇编的性能优化 - 对齐和分支预测与我之前的问题略有关系:无符号64位到双倍转换:为什么这个算法来自g ++
以下是一个不真实的测试用例.这种素性测试算法是不明智的.我怀疑任何真实世界的算法都不会执行如此多的小内循环(num大概是2**50的大小).在C++ 11中:
using nt = unsigned long long;
bool is_prime_float(nt num)
{
for (nt n=2; n<=sqrt(num); ++n) {
if ( (num%n)==0 ) { return false; }
}
return true;
}
Run Code Online (Sandbox Code Playgroud)
然后g++ -std=c++11 -O3 -S生成以下内容,包含RCX n和包含XMM6 sqrt(num).请参阅我之前发布的剩余代码(在此示例中从未执行过,因为RCX永远不会变得足够大,不能被视为带符号的否定).
jmp .L20
.p2align 4,,10
.L37:
pxor %xmm0, %xmm0
cvtsi2sdq %rcx, %xmm0
ucomisd %xmm0, %xmm6
jb .L36 // Exit the loop
.L20:
xorl %edx, %edx
movq %rbx, %rax …Run Code Online (Sandbox Code Playgroud) 看看这段代码:
one.cpp:
bool test(int a, int b, int c, int d);
int main() {
volatile int va = 1;
volatile int vb = 2;
volatile int vc = 3;
volatile int vd = 4;
int a = va;
int b = vb;
int c = vc;
int d = vd;
int s = 0;
__asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop");
__asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop");
__asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop");
__asm__("nop"); __asm__("nop"); __asm__("nop"); __asm__("nop");
for (int i=0; i<2000000000; i++) {
s += test(a, b, …Run Code Online (Sandbox Code Playgroud) 在对一些代码进行基准测试时,我发现即使是最无害的代码更改,它的执行时间也会有所不同.
我试图将下面的代码归结为最小的测试用例,但它仍然相当冗长(为此我道歉).几乎任何改变都会影响基准测试结果.
#include <string>
#include <vector>
#include <iostream>
#include <random>
#include <chrono>
#include <functional>
constexpr double usec_to_sec = 1000000.0;
// Simple convenience timer
class Timer
{
std::chrono::high_resolution_clock::time_point start_time;
public:
Timer() : start_time(std::chrono::high_resolution_clock::now()) { }
int64_t operator()() const {
return static_cast<int64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(
std::chrono::high_resolution_clock::now()-start_time).count()
);
}
};
// Convenience random number generator
template <typename T>
class RandGen
{
mutable std::default_random_engine generator;
std::uniform_int_distribution<T> distribution;
constexpr unsigned make_seed() const {
return static_cast<unsigned>(std::chrono::system_clock::now().time_since_epoch().count());
}
public:
RandGen(T min, T max) : generator(make_seed()), distribution(min, max) { } …Run Code Online (Sandbox Code Playgroud) 在ELF文件格式中,我们有一个Alignment字段,Segment Header Table即Program Header Table.
在 Windows PE 文件格式的情况下,他们将其带到下一个级别,节有两个对齐值,一个在磁盘文件中,另一个在内存中。PE 文件头指定了这两个值。
我对这种对齐一无所知。我们需要它做什么?如何以及在哪里使用它?同样,我不知道什么是二进制文件格式上下文中的对齐,但我们为什么需要它?
假设我有以下主循环
.L2:
vmulps ymm1, ymm2, [rdi+rax]
vaddps ymm1, ymm1, [rsi+rax]
vmovaps [rdx+rax], ymm1
add rax, 32
jne .L2
Run Code Online (Sandbox Code Playgroud)
我想时间的方式是把它放在另一个像这样的长循环中
;align 32
.L1:
mov rax, rcx
neg rax
align 32
.L2:
vmulps ymm1, ymm2, [rdi+rax]
vaddps ymm1, ymm1, [rsi+rax]
vmovaps [rdx+rax], ymm1
add rax, 32
jne .L2
sub r8d, 1 ; r8 contains a large integer
jnz .L1
Run Code Online (Sandbox Code Playgroud)
我发现的是我选择的对齐方式会对时序产生重大影响(最高可达+ -10%).我不清楚如何选择代码对齐方式.我可以想到三个地方,我可能想要对齐代码
triad_fma_asm_repeat下面的代码中).L1上面)重复我的主循环.L2上图). 我发现的另一件事是,如果我在源文件中放入另一个例程,即更改一条指令(例如删除指令),即使它们是独立函数,也会对下一个函数的时序产生重大影响.我甚至在过去看到过影响另一个目标文件中的例程.
我在Agner Fog的优化装配手册中阅读了第11.5节"代码对齐",但我仍然不清楚调整代码以测试性能的最佳方法.他给出了一个例子,11.5,计时内循环,我并没有真正遵循.
目前,从我的代码中获得最高性能是一种猜测不同值和对齐位置的游戏.
我想知道是否有一种智能方法可以选择对齐方式?我应该对齐内圈和外圈吗?只是内循环?该功能的入口?使用短期或长期NOP是否重要?
我最感兴趣的是Haswell,其次是SNB/IVB,然后是Core2.
我尝试了NASM和YASM,并发现这是一个显着不同的领域.NASM仅插入一个字节的NOP指令,其中YASM插入多字节NOP.例如,通过将上面的内部和外部循环对齐到32字节,NASM插入20条NOP(0x90)指令,其中YASM插入以下内容(来自objdump)
2c: 66 …Run Code Online (Sandbox Code Playgroud) x86 ×4
assembly ×3
performance ×3
benchmarking ×2
c++ ×2
gcc ×2
visual-c++ ×2
binaryfiles ×1
clang ×1
elf ×1
intel ×1
linker ×1
loader ×1
nasm ×1
x86-64 ×1
yasm ×1