我为Project Euler Q14编写了这两个解决方案,在汇编和C++中.它们是用于测试Collatz猜想的相同蛮力方法.装配解决方案与组装
nasm -felf64 p14.asm && gcc p14.o -o p14
Run Code Online (Sandbox Code Playgroud)
C++是用.编译的
g++ p14.cpp -o p14
Run Code Online (Sandbox Code Playgroud)
部件, p14.asm
section .data
fmt db "%d", 10, 0
global main
extern printf
section .text
main:
mov rcx, 1000000
xor rdi, rdi ; max i
xor rsi, rsi ; i
l1:
dec rcx
xor r10, r10 ; count
mov rax, rcx
l2:
test rax, 1
jpe even
mov rbx, 3
mul rbx
inc rax
jmp c1
even:
mov rbx, 2 …
Run Code Online (Sandbox Code Playgroud) 背景:
在使用嵌入式汇编语言优化某些Pascal代码时,我注意到了一条不必要的MOV
指令,并将其删除.
令我惊讶的是,删除不必要的指令会导致我的程序变慢.
我发现添加任意无用的MOV
指令可以进一步提高性能.
效果不稳定,并且基于执行顺序进行更改:相同的垃圾指令向上或向下移动一行会产生减速.
我知道CPU会进行各种优化和精简,但这看起来更像是黑魔法.
数据:
我的代码版本有条件地在运行时间的循环中编译三个垃圾操作2**20==1048576
.(周围的程序只计算SHA-256哈希值).
在我相当老的机器(英特尔(R)Core(TM)2 CPU 6400 @ 2.13 GHz)上的结果:
avg time (ms) with -dJUNKOPS: 1822.84 ms
avg time (ms) without: 1836.44 ms
Run Code Online (Sandbox Code Playgroud)
程序在循环中运行25次,每次运行顺序随机变化.
摘抄:
{$asmmode intel}
procedure example_junkop_in_sha256;
var s1, t2 : uint32;
begin
// Here are parts of the SHA-256 algorithm, in Pascal:
// s0 {r10d} := ror(a, 2) xor ror(a, 13) xor …
Run Code Online (Sandbox Code Playgroud) 从Pentium Pro(P6微体系结构)开始,英特尔重新设计了它的微处理器,并在旧的CISC指令下使用了内部RISC内核.由于Pentium Pro所有CISC指令都分为较小的部分(uops),然后由RISC内核执行.
一开始我很清楚英特尔决定隐藏新的内部架构并强迫程序员使用"CISC shell".由于这一决定,英特尔可以在不破坏兼容性的情况下完全重新设计微处理器架构,这是合理的.
但是我不明白一件事,为什么英特尔仍然保留了多年内隐藏的内部RISC指令集?为什么他们不让程序员使用RISC指令,比如使用旧的x86 CISC指令集?
如果英特尔长期保持向后兼容性(我们仍然在64位模式旁边有虚拟8086模式),为什么它们不允许我们编译程序以便它们绕过CISC指令并直接使用RISC核心?这将开启自然的方式来慢慢放弃x86指令集,现在已弃用(这是英特尔决定在内部使用RISC核心的主要原因,对吧?).
看看新的英特尔'酷睿i'系列,我看到,他们只扩展了CISC指令集,增加了AVX,SSE4等.
通过编码是否有任何(非微优化)性能增益
float f1 = 200f / 2
Run Code Online (Sandbox Code Playgroud)
在比较中
float f2 = 200f * 0.5
Run Code Online (Sandbox Code Playgroud)
几年前我的一位教授告诉我,浮点除法比浮点乘法慢,但没有详细说明原因.
这句话适用于现代PC架构吗?
UPDATE1
关于评论,请同时考虑这个案例:
float f1;
float f2 = 2
float f3 = 3;
for( i =0 ; i < 1e8; i++)
{
f1 = (i * f2 + i / f3) * 0.5; //or divide by 2.0f, respectively
}
Run Code Online (Sandbox Code Playgroud)
更新2 从评论中引用:
[我想]知道什么是算法/架构要求导致>除法在硬件上比复制要复杂得多
LOOP(英特尔参考手动输入)递减ecx/rcx,然后如果非零则跳转.这很慢,但是英特尔不能廉价地把它变得很快吗? dec/jnz
已经将宏观融合成 Sandybridge家族的一个 uop; 唯一的区别是设置标志.
loop
关于各种微体系结构,来自Agner Fog的说明表:
Bulldozer-family/Ryzen:1 m-op(与宏观融合测试和分支相同,或者jecxz
)
P4:4次(相同jecxz
)
loope
/ loopne
).吞吐量= 4c(loop
)或7c(loope/ne
).loope
/ loopne
). 吞吐量=每5个循环一个,这是将循环计数器保留在内存中的瓶颈!jecxz
只有2 uops,吞吐量与普通吞吐量相同jcc
难道解码器不能像lea rcx, [rcx-1]
/ 那样解码jrcxz
吗?这将是3 uops.至少那是没有地址大小前缀的情况,否则它必须使用ecx
和截断RIP
,EIP
如果跳转; 也许奇怪的地址大小选择控制减量的宽度解释了许多uops?
或者更好,只需将其解码为不设置标志的融合分支和分支? dec ecx …
我使用英特尔®架构代码分析器(IACA)发现了一些意想不到的东西(对我而言).
以下指令使用[base+index]
寻址
addps xmm1, xmmword ptr [rsi+rax*1]
Run Code Online (Sandbox Code Playgroud)
根据IACA没有微熔丝.但是,如果我用[base+offset]
这样的
addps xmm1, xmmword ptr [rsi]
Run Code Online (Sandbox Code Playgroud)
IACA报告它确实融合了.
英特尔优化参考手册的第2-11节给出了以下"可以由所有解码器处理的微融合微操作"的示例
FADD DOUBLE PTR [RDI + RSI*8]
Run Code Online (Sandbox Code Playgroud)
和Agner Fog的优化装配手册也给出了使用[base+index]
寻址的微操作融合的例子.例如,请参见第12.2节"Core2上的相同示例".那么正确的答案是什么?
我目前编码的一些C99标准库字符串函数高度优化的版本,例如strlen()
,memset()
等等,采用x86-64的组件,SSE-2指令.
到目前为止,我已经在性能方面取得了很好的成绩,但是当我尝试优化更多时,我有时会遇到奇怪的行为.
例如,添加或甚至删除一些简单的指令,或者只是重新组织一些用于跳转的本地标签会完全降低整体性能.在代码方面绝对没有理由.
所以我的猜测是代码对齐存在一些问题,和/或有错误预测的分支.
我知道,即使使用相同的架构(x86-64),不同的CPU也有不同的分支预测算法.
但是,在开发x86-64的高性能时,是否存在一些关于代码对齐和分支预测的一般建议?
特别是关于对齐,我应该确保跳转指令使用的所有标签都在DWORD上对齐吗?
_func:
; ... Some code ...
test rax, rax
jz .label
; ... Some code ...
ret
.label:
; ... Some code ...
ret
Run Code Online (Sandbox Code Playgroud)
在前面的代码中,我之前应该使用align指令.label:
,例如:
align 4
.label:
Run Code Online (Sandbox Code Playgroud)
如果是这样,使用SSE-2时是否足以对齐DWORD?
关于分支预测,是否有一种"优先"的方式来组织跳转指令使用的标签,以帮助CPU,或者今天的CPU是否足够聪明,可以通过计算分支的计数来确定在运行时?
编辑
好的,这是一个具体的例子 - 这是strlen()
SSE-2 的开始:
_strlen64_sse2:
mov rsi, rdi
and rdi, -16
pxor xmm0, xmm0
pcmpeqb xmm0, [ rdi ]
pmovmskb rdx, xmm0
; ...
Run Code Online (Sandbox Code Playgroud)
使用1000个字符串运行10'000'000次约为0.48秒,这很好.
但它不会检查NULL字符串输入.显然,我会添加一个简单的检查:
_strlen64_sse2:
test rdi, rdi
jz .null
; ... …
Run Code Online (Sandbox Code Playgroud) 引用英特尔 ®64 和IA-32架构优化参考手册,§2.4.6"REP String Enhancement":
使用REP字符串的性能特征可归因于两个组件: 启动开销和数据传输吞吐量.
[...]
对于较大粒度数据传输的REP字符串,随着ECX值的增加,REP String的启动开销呈逐步增加:
- 短串(ECX <= 12):REP MOVSW/MOVSD/MOVSQ的延迟约为20个周期,
快速字符串(ECX> = 76:不包括REP MOVSB):处理器实现通过移动尽可能多的16字节数据来提供硬件优化.如果其中一个16字节数据传输跨越缓存行边界,则REP字符串延迟的延迟会有所不同:
- 无拆分:延迟包括大约40个周期的启动成本,每个64字节的数据增加4个周期,
- 高速缓存拆分:延迟包括大约35个周期的启动成本,每64个字节的数据增加6个周期.
中间字符串长度:REP MOVSW/MOVSD/MOVSQ的延迟具有大约15个周期的启动成本加上word/dword/qword中数据移动的每次迭代的一个周期.
(强调我的)
没有进一步提及这种启动成本.它是什么?它做了什么,为什么总是需要更多的时间?
看看这段代码:
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) 如果我编译,使用g ++ 4.9.2
bool int_dbl_com(const unsigned long long x, const double y)
{
return x <= y;
}
Run Code Online (Sandbox Code Playgroud)
那么汇编程序输出(对于Windows x64调用约定)是:
testq %rcx, %rcx # x in RCX
js .L2
pxor %xmm0, %xmm0
cvtsi2sdq %rcx, %xmm0
ucomisd %xmm0, %xmm1 # y in XMM1
setae %al
ret
Run Code Online (Sandbox Code Playgroud)
该命令cvtsi2sdq
是签名转换,第一个测试和跳转组合是检查是否%rcx < 0
.如果是这样,我们去L2,我不明白:
.L2:
movq %rcx, %rax
andl $1, %ecx
pxor %xmm0, %xmm0
shrq %rax
orq %rcx, %rax
cvtsi2sdq %rax, %xmm0
addsd %xmm0, %xmm0
ucomisd %xmm0, %xmm1
setae %al …
Run Code Online (Sandbox Code Playgroud) assembly ×7
performance ×6
x86 ×5
x86-64 ×4
intel ×3
optimization ×3
c++ ×2
benchmarking ×1
clang ×1
cpu ×1
freepascal ×1
g++ ×1
gcc ×1
iaca ×1
sse2 ×1