MASM 使用 VS 击败未优化的 .cpp 但不是未优化的 .c
Posted
技术标签:
【中文标题】MASM 使用 VS 击败未优化的 .cpp 但不是未优化的 .c【英文标题】:MASM beats unoptimised .cpp but not unoptimised .c using VS 【发布时间】:2016-01-16 23:30:33 【问题描述】:我有一个非常简单的函数,它使用行主矩阵 (float**) 转换向量 (float*):
int vector_by_matrix(float** m, float* v, float* out, int size)
int i, j;
float temp;
if (!m || !v || !out) return -1;
for (i = 0; i < size; i++)
temp = 0;
for (j = 0; j < size; j++)
temp += m[i][j] * v[j];
//out[i] = temp * v[i]; MISTAKE DURING COPYING - SHOULD'VE BEEN...
out[i] = temp;``
return 0;
代码最初是使用 Visual Studio (2013) C++ 编译器编译为 C++ (x64) 的;并且没有优化非常慢(该函数在运行期间被调用了数百/数千次,并且系统的大小通常很大 c. size = 10000)。将优化设置为高 (O2) 并将浮点模式设置为 fast,性能提升是巨大的 (x20)。但是,我决定将文件转换为 .c 源文件并再次使用 VS 编译为 C - 无论如何它都是简单的程序代码。无论是否进行优化,性能都会再次提高(通过优化的 C++ 编译)。事实上,优化设置对性能影响不大。
我不明白为什么 C 代码总是更快(优化/未优化)。我反汇编了 C(/C++) 编译器的输出,它看起来很可怕——我最初在 MASM 中编写了相同的函数,它大约是代码的五分之一,但在速度方面无法竞争。 VS 是否总是优化已编译的 C 代码?从反汇编的代码来看,它确实看起来像,但我不能确定。如果有帮助,我的 MASM 代码:
mul_vector_by_martix proc
mov r10, r9
sub rsp, 8
mov qword ptr[rsp], r11
LI:
MOV rbx, qword ptr[r10*8+rcx[0]-8]
XORPS xmm0, xmm0
mov r11, r9
LJ:
MOVSS xmm1, dword ptr[r11*4+rbx[0]-4]
MULSS xmm1, dword ptr[r11*4+rdx[0]-4]
ADDSS xmm0, xmm1
sub r11, 1
jnz LJ
MOVSS dword ptr[r10*4+r8[0]-4], xmm0
sub r10, 1
jnz LI
mov r11, qword ptr[rsp]
add rsp, 8
ret
mul_vector_by_martix endp
我不会提供反汇编代码 - 问题已经够长了 ;)
提前感谢您的帮助。
更新
我今天又来调查这个问题。我已经实现了打包指令(当前实现仅适用于系统大小为 4 的倍数,否则您可能会崩溃):
mul_opt_vector_by_martix proc
sub rsp, 8
mov qword ptr[rsp], r12
sub rsp, 8
mov qword ptr[rsp], r13
; copy rdx for arithmetic operations
mov r10, rdx
; init static global
mov r12, LSTEP
cmp VSIZE, r9
je LOOPS
; get sizeof(vector)
mov rax, 4
mul r9
mov r12, rax
; get the number of steps in inner loop
mov r11, 16
mov rax, r12
div r11
mov r11, rax
mov r12, r11
mov rax, 16
mul r12
mov r12, rax
sub r12, 16
mov VSIZE, r9
mov LSTEP, r12
LOOPS:
LI:
MOV rbx, qword ptr[r9*8+rcx[0]-8]
XORPS xmm0, xmm0
mov r13, r12
LJ:
MOVAPS xmm1, xmmword ptr[r13+rbx[0]]
MULPS xmm1, xmmword ptr[r13+r10[0]]
; add the packed single floating point numbers together
MOVHLPS xmm2, xmm1
ADDPS xmm2, xmm1
MOVAPS xmm1, xmm2
SHUFPS xmm2, xmm2, 1 ; imm8 = 00 00 00 01
ADDSS xmm2, xmm1
ADDSS xmm0, xmm2
sub r13, 16
cmp r13, 0
JGE LJ
MOVSS dword ptr[r9*4+r8[0]-4], xmm0
sub r9, 1
jnz LI
mov r13, qword ptr[rsp]
add rsp, 8
mov r12, qword ptr[rsp]
add rsp, 8
ret
mul_opt_vector_by_martix endp
它改进了大约 20-30%,但又无法与未优化的编译 C 代码竞争。内循环的反汇编代码:
sum += v[j] * m[i][j];
movsxd rax,r8d
add rdx,8
movups xmm0,xmmword ptr [rbx+rax*4]
movups xmm1,xmmword ptr [r10+rax*4]
lea eax,[r8+4]
movsxd rcx,eax
add r8d,8
mulps xmm1,xmm0
movups xmm0,xmmword ptr [rbx+rcx*4]
addps xmm2,xmm1
movups xmm1,xmmword ptr [r10+rcx*4]
mulps xmm1,xmm0
addps xmm3,xmm1
cmp r8d,r9d
jl vector_by_matrix+90h (07FEDD321440h)
addps xmm2,xmm3
movaps xmm1,xmm2
movhlps xmm1,xmm2
addps xmm1,xmm2
movaps xmm0,xmm1
shufps xmm0,xmm1,0F5h
addss xmm1,xmm0
在这一点上,我不得不承认我看不到收益在哪里。我没有费心将代码重建为 C++ 以查看程序集是否不同,但我怀疑在未优化模式下,C++ 不会像 C 对 VS 编译器那样适合快速代码。也许 Frankie_C 的观点是中肯的。不过令人担忧的是,如果编译器正在做它不应该做的事情——不过我看不出有什么错误;以我的经验,任何半体面的手写程序集都将胜过未优化的 C,但在此编译器中则不然。浮点运算需要对精度问题进行严格控制,否则结果可能因一台机器而异,而且需要收敛的方法甚至可能在一台机器上失败,但由于不稳定性而不会在另一台机器上失败。
更新2============================================= =========================
这似乎很安静,但我想如果我有任何改进,我会告诉大家。好吧,我可以通过重新排列循环中的一些操作来匹配编译器,如上次更新中所示。很明显,只需将 - 打包 - 改组和添加到内部循环之外。同样由于“矢量化”的隐式大小,系统的大小必须是 4 的倍数(否则会崩溃)。
LOOPS:
LI:
MOV rbx, qword ptr[r9*8+rcx[0]-8]
XORPS xmm0, xmm0
mov r13, r12
LJ:
MOVAPS xmm1, xmmword ptr[r13+rbx[0]]
MULPS xmm1, xmmword ptr[r13+r10[0]]
; just add and accrue
ADDPS xmm0, xmm1
sub r13, 16
cmp r13, 0
jge LJ
;------------ moved this block to the outside --------------;
; add the packed single floating point numbers together
MOVHLPS xmm1, xmm0
ADDPS xmm1, xmm0
MOVAPS xmm0, xmm1
SHUFPS xmm1, xmm1, 1 ; imm8 = 00 00 00 01
ADDSS xmm0, xmm1
;--------------------end block---------------------------
MOVSS dword ptr[r9*4+r8[0]-4], xmm0
sub r9, 1
jnz LI
仍然无法击败编译器,但非常接近等于它。我想结论是,即使是未优化的 C,也很难击败 VS 编译器——这不是我对(未优化的代码)其他编译器(如 gcc)的经验。 我可以通过使用带有更多 xmm regsiters 的 SIMD 指令展开循环来超越编译器。我可以根据要求提供这个,但它可能是不言自明的。
【问题讨论】:
“C 编译器反汇编太可怕了”是什么意思?编译器技术赋予指令执行时间特权,而且,令人难以置信的是,一些使用更多指令的结构运行得更快。 C++ 受到庞大而复杂的语言框架的影响,限制了优化并在可执行文件中放入了许多无用的代码。 C 是线性和裸代码。 @Frankie_C:好吧,我不同意第二部分。 C 是“线性的”取决于编译器。也就是说:编译器(如果得到正确指示)使用所有可能的技巧,从内存对齐到重新排序单个指令。即使基本算法是合理的,也很难被人类编写程序击败。 感谢 Frankie_C。我知道延迟开销可能会变得很大,一些指令更不用说使代码容易出现缓存未命中等。但我仍然不明白为什么未优化的代码和优化的代码总是更快。反汇编的代码甚至没有使用堆栈指针。顺便说一句,我不希望用我自己的 asm 能够超越现代编译器的优化;但未优化的代码? Jonhware 我同意这一切,我并不是说我应该能够使用优化的编译器来完成。但是我应该能够击败未优化的编译器,并且如果编译器正在做一些我没有告诉它的事情 - 它不应该对机器之间的数值稳定性产生巨大影响。未优化的 C++ 代码比我的 MASM 代码慢。所以我不明白为什么未优化的编译 C 不是这样。 如果你真的对速度感兴趣,除了“奇怪”,看看this是否更好(它确实对大小做了一些额外的假设,但你可以用零填充使其成为现实)(另外,使用更多的累加器可能会更好,尤其是在 Haswell 上)。如果您查看生成的程序集,您应该能够在原始代码上击败编译器。 【参考方案1】:基准测试比这更棘手。
例如,使用 clang,以下代码编译为 完全 与 main 中相同的代码,不管对 vector_by_matrix
的调用是否被注释掉。
#include <algorithm>
#include <numeric>
int main()
using namespace std;
auto constexpr N = 512;
float* m[N];
generate_n(m, N, []return new float[N];);
float v[N], out[N];
float start = 0.0;
for(auto& col : m) iota(col, col+N, start += 0.1);
iota(begin(v), end(v), -1.0f);
//vector_by_matrix(m, v, out, N);
for_each(begin(m), end(m), [](float*p) delete[] p; );
编译器识别出没有可观察到的行为发生变化,因此它可以将其排除在外。
当然,只要您实际检查组件,就应该没问题。 (虽然,如果vector_by_matrix
函数被标记为静态文件,它甚至不会出现在列表中:))。
但是,如果您要进行任何测量,请确保您使用了可靠的统计分析,并且正在测量您认为您正在测量的内容。
查看程序集:
gcc 5.3:https://goo.gl/wIvWsE gcc 5.3 调用评论:https://goo.gl/Z9hLsZ clang 3.7:https://goo.gl/xidrS6 clang 3.7 调用评论:https://goo.gl/gUc4Ux完整列表供参考
int vector_by_matrix(float** m, float *const v, float *out, int size)
int i, j;
float temp;
if (!m || !v || !out)
return -1;
for (i = 0; i < size; i++)
temp = 0;
for (j = 0; j < size; j++)
temp += m[i][j] * v[j];
out[i] = temp * v[i];
return 0;
#include <algorithm>
#include <numeric>
int main()
using namespace std;
auto constexpr N = 512;
float* m[N];
generate_n(m, N, []return new float[N];);
float v[N], out[N];
float start = 0.0;
for(auto& col : m) iota(col, col+N, start += 0.1);
iota(begin(v), end(v), -1.0f);
vector_by_matrix(m, v, out, N); // NO DIFFERENCE IF COMMENTED
for_each(begin(m), end(m), [](float*p) delete[] p; );
【讨论】:
谢谢。我已经通过分析器运行了这个,瓶颈在这些函数中。 如果您显示该代码,您将更有说服力。也许有探查器结果 您在运行分析器时使用的代码,您可以从中得出这些结论。这是缺少的部分。我很乐意提供帮助(验证/改进基准代码),但我需要这些信息 最初的基准测试是使用 .NET 诊断命名空间中的秒表类粗略完成的。从 dll 函数调用和返回之间的时间被重新编码为微秒:分析器在可执行文件上运行,它具有精细的粒度,但仅基于函数报告,但在代码的红色部分突出显示最昂贵的部分(尽管没有度量我可以看到,仅用于功能)。我的 MASM 代码的突出显示部分是 ADDSS 调用,但这些调用更频繁地出现在反汇编的二进制文件中。我单次运行的平均时间:C (2.1) vs MASM (3.8)。 我问过 >whatgithub.com/rmartinho/nonius以上是关于MASM 使用 VS 击败未优化的 .cpp 但不是未优化的 .c的主要内容,如果未能解决你的问题,请参考以下文章