我为Project Euler Q14编写了这两个解决方案,用汇编和c++。他们采用了相同的蛮力方法来测试Collatz猜想。装配方案由:
nasm -felf64 p14.asm && gcc p14.o -o p14
c++是用以下工具编译的:
g++ p14.cpp -o p14
组装、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
xor rdx, rdx
div rbx
c1:
inc r10
cmp rax, 1
jne l2
cmp rdi, r10
cmovl rdi, r10
cmovl rsi, rcx
cmp rcx, 2
jne l1
mov rdi, fmt
xor rax, rax
call printf
ret
c++, p14.cpp:
#include <iostream>
int sequence(long n) {
int count = 1;
while (n != 1) {
if (n % 2 == 0)
n /= 2;
else
n = 3*n + 1;
++count;
}
return count;
}
int main() {
int max = 0, maxi;
for (int i = 999999; i > 0; --i) {
int s = sequence(i);
if (s > max) {
max = s;
maxi = i;
}
}
std::cout << maxi << std::endl;
}
我知道编译器优化可以提高速度,但是我没有看到很多方法来进一步优化我的汇编解决方案(从编程的角度说,而不是从数学角度说)。
c++代码每一项使用模数,每一项使用除法,而汇编代码每一项只使用一个除法。
但是这个程序集比c++解决方案平均要长1秒。为什么会这样?我问这个问题主要是出于好奇。
执行时间
我的系统:1.4 GHz Intel Celeron 2955U上的64位Linux (Haswell微架构)。
g++(未优化):平均1272毫秒。
g++ -O3:平均578毫秒。
Asm (div)(原始):平均2650毫秒。
Asm (shr):平均679 ms。
@johnfound asm(与NASM组装):平均501毫秒。
@hidefromkgb asm:平均200毫秒。
@hidefromkgb asm,由@Peter Cordes优化:平均145毫秒。
@Veedrac c++: -O3平均81毫秒,-O0平均305毫秒。
声称c++编译器可以产生比合格的汇编语言程序员更优的代码是一个非常严重的错误。尤其是在这种情况下。人类总是能比编译器编写出更好的代码,这种特殊情况很好地说明了这一点。
您看到的时间差异是因为问题中的汇编代码在内部循环中远远不是最优的。
(下面的代码是32位的,但可以很容易地转换为64位)
例如,序列函数可以优化到只有5条指令:
.seq:
inc esi ; counter
lea edx, [3*eax+1] ; edx = 3*n+1
shr eax, 1 ; eax = n/2
cmovc eax, edx ; if CF eax = edx
jnz .seq ; jmp if n<>1
整个代码看起来像这样:
include "%lib%/freshlib.inc"
@BinaryType console, compact
options.DebugMode = 1
include "%lib%/freshlib.asm"
start:
InitializeAll
mov ecx, 999999
xor edi, edi ; max
xor ebx, ebx ; max i
.main_loop:
xor esi, esi
mov eax, ecx
.seq:
inc esi ; counter
lea edx, [3*eax+1] ; edx = 3*n+1
shr eax, 1 ; eax = n/2
cmovc eax, edx ; if CF eax = edx
jnz .seq ; jmp if n<>1
cmp edi, esi
cmovb edi, esi
cmovb ebx, ecx
dec ecx
jnz .main_loop
OutputValue "Max sequence: ", edi, 10, -1
OutputValue "Max index: ", ebx, 10, -1
FinalizeAll
stdcall TerminateAll, 0
为了编译这段代码,需要使用FreshLib。
在我的测试中,(1 GHz AMD A4-1200处理器),上面的代码大约比问题中的c++代码快四倍(当用-O0编译时:430毫秒vs. 1900毫秒),当用-O3编译时,快两倍多(430毫秒vs. 830毫秒)。
两个程序的输出是相同的:在i = 837799上,max sequence = 525。
评论:
但是,这段代码从未停止(因为整数溢出)!?!伊夫Daoust
对于许多数字,它不会溢出。
如果它会溢出——对于那些不幸的初始种子之一,溢出的数字很可能会收敛到1而不会再次溢出。
这仍然提出了一个有趣的问题,是否存在一些溢出循环的种子数?
任何简单的最终收敛级数都以2的幂值开始(够明显了吗?)
2^64会溢出到零,根据算法,这是一个未定义的无限循环(仅以1结束),但由于shr rax产生ZF=1, answer中的最优解将结束。
能得到2^64吗?如果起始数是0x5555555555555555,它是奇数,那么下一个数是3n+1,它是0xFFFFFFFFFFFFFFFF +1 = 0。理论上算法处于未定义状态,但johnfound的优化答案在ZF=1时退出即可恢复。cmp rax, Peter Cordes的1将在无限循环中结束(QED变体1,“cheapo”通过未定义的0数)。
How about some more complex number, which will create cycle without 0?
Frankly, I'm not sure, my Math theory is too hazy to get any serious idea, how to deal with it in serious way. But intuitively I would say the series will converge to 1 for every number : 0 < number, as the 3n+1 formula will slowly turn every non-2 prime factor of original number (or intermediate) into some power of 2, sooner or later. So we don't need to worry about infinite loop for original series, only overflow can hamper us.
我只是把一些数字写在表格里,然后看了看8位截断的数字。
有三个值溢出到0:227、170和85(85直接到0,其他两个递增到85)。
但是创建循环溢出种子没有任何价值。
有趣的是,我做了一个检查,这是第一个遭受8位截断的数字,已经有27个受到影响!在正确的非截断序列中,它确实达到了值9232(第一个截断值在第12步中是322),在非截断的方式中,2-255输入数字中的任何一个达到的最大值是13120(对于255本身),收敛到1的最大步数大约是128(+-2,不确定“1”是否算数,等等……)
有趣的是(对我来说)数字9232是许多其他源数字的最大值,它有什么特别之处?:-O 9232 = 0x2410…嗯. .不知道。
不幸的是,我无法深入了解这个系列,为什么它会收敛,截断它们到k位的含义是什么,但随着cmp数,1终止条件,当然可以将算法放入无限循环,特定的输入值在截断后以0结束。
但是对于8位的情况,值27溢出有点警告,这看起来像如果你计算到达值1的步数,你会得到错误的结果,对于整个k位整数集的大多数数字。对于8位整数,256个数字中的146个数字通过截断影响了序列(其中一些可能仍然意外地命中了正确的步数,我太懒了,没有检查)。