SSE:质量整数转换+SSE 比 FPU 慢?

Posted

技术标签:

【中文标题】SSE:质量整数转换+SSE 比 FPU 慢?【英文标题】:SSE: Mass integer conversion+multiply slower with SSE than FPU? 【发布时间】:2014-12-23 19:34:48 【问题描述】:

我正在开发一个经常需要将 6 到 8 个有符号 32 位整数转换为 32 位实数的应用程序。我用自定义汇编代码替换了 delphi 代码,令我惊讶的是,FPU 转换总是一样快,并且在某些计算机上比 SSE 转换快很多。下面是一些说明代码:

program Project1;

$R *.res

uses
 windows,dialogs,sysutils;

type
 piiii=^tiiii;
 tiiii=record i1,i2,i3,i4:longint; end;
 pssss=^tssss;
 tssss=record s1,s2,s3,s4:single; end;

var
 convert_value:single=13579.02468;

function convert_x87(adata:longint):single;
asm
 mov [esp-4],eax
 fild longint([esp-4])
 fmul [convert_value]
end;

procedure convert_sse(afrom,ato,aconv:pointer);
asm
 CVTDQ2PS xmm0,[eax]
 mulps xmm0,[ecx]
 movaps [edx],xmm0
end;

procedure get_mem(var p1,p2:pointer);
begin
 getmem(p1,31);
 p2:=pointer((longint(p1)+15) and (not 15));
end;

var
 a,b,c,d:cardinal;
 z:single;
 i:piiii;
 s1,s2:pssss;
 w1,w2,w3:pointer;
begin
 b:=gettickcount;
 a:=0;
 repeat
  z:=convert_x87(a);

  inc(a);
 until a=0;
 c:=gettickcount-b;

 get_mem(pointer(w1),pointer(i));
 get_mem(pointer(w2),pointer(s1));
 get_mem(pointer(w3),pointer(s2));

 s1.s1:=convert_value;
 s1.s2:=convert_value;
 s1.s3:=convert_value;
 s1.s4:=convert_value;

 b:=gettickcount;
 i.i1:=0;
 i.i2:=1;
 i.i3:=2;
 i.i4:=3;
 repeat
  convert_sse(i,s2,s1);

  inc(i.i1,4);
  inc(i.i2,4);
  inc(i.i3,4);
  inc(i.i4,4);
 until i.i1=0;
 d:=gettickcount-b;

 freemem(w1);
 freemem(w2);
 freemem(w3);

 showmessage('FPU:'+inttostr(c)+'/SSE:'+inttostr(d));
end.

在转换过程中需要重新缩放(所以是乘法),这就是为什么里面有一个。使用的值只是我选择的一个随机值,但无论我使用什么值,结果都是一样的。 FPU 和 SSE 之间的舍入也有非常小的差异,但在这种情况下并不重要。

但是,如果您运行该代码,您会发现 FPU 路径永远不会比 SSE 路径慢,而且它没有任何意义。有人知道发生了什么吗?


编辑:这是汇编程序中带有循环的不同源代码。结果真的很有趣。如果注释掉增量指令,SSE 版本比 FPU 版本快很多,但如果包含增量指令,则它们的速度大致相同:

program Project1;

$R *.res

uses
 windows,dialogs,sysutils;

type
 piiii=^tiiii;
 tiiii=record i1,i2,i3,i4:longint; end;
 pssss=^tssss;
 tssss=record s1,s2,s3,s4:single; end;

var
 convert_value:single=13579.02468;

procedure test_convert_x87;
asm
 // init test data
 push ebx
 xor ebx,ebx

 mov [esp-4],$98765432

 // convert and multiply 1 int32 to 1 single
@next_loop:
// inc [esp-4]
 fild longint([esp-4])
 fmul [convert_value]
 fstp single([esp-8])

 // loop
 dec ebx
 jnz @next_loop

 pop ebx
end;

procedure test_convert_sse(afrom,ato,aconv:pointer);
asm
 // init test data
 push ebx
 xor ebx,ebx

 mov [eax+0],$98765432
 mov [eax+4],$98765432
 mov [eax+8],$98765432
 mov [eax+12],$98765432

 // convert and multiply 4 int32 to 4 single
@next_loop:
// inc [eax+0]
// inc [eax+4]
// inc [eax+8]
// inc [eax+12]
 cvtdq2ps xmm0,[eax]
 mulps xmm0,[ecx]
 movaps [edx],xmm0

 // loop
 sub ebx,4
 jnz @next_loop

 pop ebx
end;

procedure get_mem(var p1,p2:pointer);
begin
 getmem(p1,31);
 p2:=pointer((longint(p1)+15) and (not 15));
end;

var
 b,c,d:cardinal;
 i:piiii;
 s1,s2:pssss;
 w1,w2,w3:pointer;
begin
 b:=gettickcount;
 test_convert_x87;
 c:=gettickcount-b;

 get_mem(pointer(w1),pointer(i));
 get_mem(pointer(w2),pointer(s1));
 get_mem(pointer(w3),pointer(s2));

 s1.s1:=convert_value;
 s1.s2:=convert_value;
 s1.s3:=convert_value;
 s1.s4:=convert_value;

 b:=gettickcount;
 test_convert_sse(i,s2,s1);
 d:=gettickcount-b;

 freemem(w1);
 freemem(w2);
 freemem(w3);

 showmessage('FPU:'+inttostr(c)+'/SSE:'+inttostr(d));
end.

【问题讨论】:

请贴出快慢版生成的汇编代码。这使得找到罪魁祸首变得更加容易,因为这里很少有人使用 pascal 并且可以轻松地重新创建您的情景。 您好,感谢您的关注。如果您检查名为 convert_x87 和 convert_sse 的函数的发布源,您会看到它们在汇编程序中,您应该能够复制粘贴它们。只有计时部分在delphi中。 名为 convert_sse 的函数一次执行 4 次转换 + 乘法运算,并且与一次执行 1 次的 convert_fpu 函数调用 4 次的速度更慢或相同。 convert_sse 函数中使用的指令是 CVTDQ2PS mulps movaps,它们不是适合此任务的 SIMD 指令吗? @Marladu 周围的代码(循环)非常重要。无论编译器做什么,简单的三行汇编代码都可能完全被破坏。我很难学到这一点。 在第一个程序中,注释掉对convert_sseconvert_x87 的调用,您会发现x87 变体要快得多。所有这些代码所做的就是计数到2**32。实际上,sse 变体花费了 25% 的时间来计算。比 x87 版本大得多的百分比。我对此的看法是,您代码的 FP 部分是微不足道的。您是否 100% 确定这是您的瓶颈?只有当您在程序中除了从整数转换为浮点数之外什么都不做时,您才能期望提高性能。实际程序中进行转换所花费的时间百分比是多少? 【参考方案1】:

asm 看起来很慢的主要原因是没有将内容保存在寄存器中。 4 个连续的内存位置中的 4 个inc 是疯狂的,难怪它很慢。特别是。如果您下次只是要再次从内存中读回它们。在循环外设置循环计数器向量,然后通过向其添加 1, 1, 1, 1 向量来递增它。

您的问题也没有任何关于 32 位 Windows 调用约定是什么的提醒(哪个 arg 进入哪个寄存器),所以我必须通过查看您的函数 arg 变量名称与您的使用方式来弄清楚这一点他们。

所以你的内部循环可以是这样的:

; *untested*
    movdqa xmm1, [ vector_of_ones ]   ; or pcmpgt same,same -> all 1s, packed right shift by 32bits
    xor ebx, ebx  ; loop counter
;  also broadcast the scale value to xmm4, maybe with shufps
    movdqa   xmm2, [eax]   ; values to be incremented and converted
loop:
    cvtdq2ps xmm0, xmm2
    mulps    xmm0, xmm4  ; scale
    movaps   [edx], xmm0
    paddd    xmm2, xmm1  ; increment counters
    sub      ebx, 4
    jne      loop  ; loop 2^32 times

    ; movdqa    [eax], xmm2   ; store the incremented loop counter?
    ;  Not sure if this was desired, or a side effect of using mem instead of regs.
    ; If you want this to work on an array, put this store in the loop
    ; and use an indexed addressing mode for eax and edx (or increment pointers)

如果这是一个不会循环的函数,那么为mulps 设置比例向量是不同的。理想情况下,scale arg 应该在向量寄存器的低元素中传递,然后你从那里用shufps 或其他东西广播它。如果 delphi 强制它像 GP 寄存器指向的内存一样出现,那么我猜首先是 movss。如果它是编译时常量,则使用 16B 向量常量作为 mulps 的内存操作数可能是可行的方法。 Core2 和更高版本只需要一个周期来加载 128b。 (不过,对于旧 CPU 上的非 AVX 矢量内容,它确实需要对齐。)

无论如何,我认为您的基准测试速度慢的主要原因是内存访问,尤其是写入。每个周期只能有一个商店。如果 delphi 不能在寄存器中传递浮点参数,那就太糟糕了。

【讨论】:

嗨,彼得,感谢您的时间和精力。自从我发布这个问题以来已经有很长一段时间了,我认为我正在对我的学习尝试进行基准测试,并最终做出了一个设计非常糟糕的基准测试。这个周末我会好好阅读这个答案,我还有很多东西要学,所以再次感谢您花时间回答。 如果你回到这个问题,试着用 asm 写一个完整的内部循环。在实际使用以及基准测试中,非内联函数调用或调用约定的数据移动可能会支配实际函数体所花费的时间。

以上是关于SSE:质量整数转换+SSE 比 FPU 慢?的主要内容,如果未能解决你的问题,请参考以下文章

平衡 SSE 和 FPU

SSE 内在函数:将 32 位浮点数转换为 UNSIGNED 8 位整数

SSE FPU 并行

如何使用 SSE 将 _m128i 转换为无符号整数?

AVX mat4 inv 实现比 SSE 慢

SSE 从 __m128 中提取整数用于索引数组