最近无聊中一不留神又在网上看到了Quake中的那段著名的Inverse Square Root代码,忽然产生了一个冲动,将那段代码改成了一个计算浮点除法的函数,不幸就得到了一段比两个浮点数直接相除更快的代码。先说说我是怎么改造的,这段Quake代码计算的是某个浮点数的平方根的倒数,如果我们用这段代码对除数进行处理,得到的结果平方一下,再乘以被除数,那么结果当然就是两个数直接相除的值了。看上去很白痴的行为,可是结果却出人意料,绕这么一大圈来得到结果,比直接两个数相除还快。
简单测试了一下,又发现了另一件有趣的事情,在Intel Core Duo cpu上我的函数大约快50%,可是在Intel P4上我的函数竟然快了220%(很狼狈的结果,在两种cpu上我的函数执行时间几乎相同),看来传说中P4有时比P3还慢的谣言并不是瞎说。下面详细解说一下我的改造:先看一下传说中的那个Inverse sqrt函数的相当神奇的源码:
float InvSqrt(float x){
   float xhalf = 0.5f * x;
   int i = *(int*)&x; // store floating-point bits in integer
   i = 0x5f3759d5 - (i >> 1); // initial guess for Newton's method
   x = *(float*)&i; // convert new bits into float
   x = x*(1.5f - xhalf*x*x); // One round of Newton's method
   return x;
}改造第一步,当然是将其翻译成delphi:
function InvSqrt(x: Single): Single;
var
  xhalf: Single;
  i: Integer;
begin
  xhalf := x * 0.5;
  i := PInteger(@xhalf)^;
  i := $5f3759d5 - (i shr 1);
  x := PSingle(@i)^;
  Result := x * (1.5 - xhalf*x*x);
end;由于本人对执行效率的变态追求(并不是一个好习惯),我又进一步将其改成了汇编:
var
  OneHalf:       DWORD  = $3FC00000;function f_invsqrt(v: Single): Single;
asm
         MOV        EAX, DWORD PTR [EBP+8]  // i := PInteger(@x)^;
         SUB        EAX, $800000            
         PUSH       EAX                     // xhalf := x*2
         ADD        EAX, $800000            
         MOV        EDX, $5F3759D5          
         SHR        EAX, 1
         SUB        EDX, EAX                // i := 5f3759d5-(i shr 1)
         PUSH       EDX
         FLD        DWORD PTR [ESP]         // x := PSingle(@i)^
         FLD        ST(0)
         FMUL       ST(0), ST(0)
         FMUL       DWORD PTR [ESP+4]
         FSUBR      DWORD PTR OneHalf
         FMULP                              // result := x * (1.5 - xhalf*x*x);
         ADD        ESP, 8
end;再将其改成除法, 用delphi写的话就是(当然,这段代码绝对比直接除慢得多的多的多):
function f_div(x, y: Single): Single;
begin
  if y<0 then 
  begin
    y := -y;
    x := -x;
  end;
  result :=f_invsqrt(y) * f_invsqrt(y) * x;
end;最后优化后的代码如下:function f_div(v1, v2: Single): Single;
asm
         /* 调整v1, v2的值: v2取绝对值并相应调整v1的正负 */
         MOV        EAX, DWORD PTR [EBP+8]  
         MOV        EDX, $80000000
         AND        EDX, EAX                
         AND        EAX, $7FFFFFFF          
         XOR        DWORD PTR [EBP+12], EDX 
         FLD        DWORD PTR [EBP+12]
         /* 计算v2的Inverse sqrt再平方再乘以v1 */
         SUB        EAX, $800000
         PUSH       EAX
         ADD        EAX, $800000
         MOV        EDX, $5F3759D5
         SHR        EAX, 1
         SUB        EDX, EAX
         PUSH       EDX
         FLD        DWORD PTR [ESP]
         FMUL       ST(0), ST(0)
         FMUL       ST(1), ST(0)
         FMUL       DWORD PTR [ESP+4]
         FSUBR      DWORD PTR OneHalf
         FMUL       ST(0), ST(0)
         FMULP
         ADD        ESP, 8
end;

解决方案 »

  1.   

    先看看,感觉FDIV没你说的那么慢,测试一下。
      

  2.   

    测试了一下,发现几个问题:1. f_div代码精度比较差,一般只能精确到小数点后2~3位,某些情况下更差,而单精度浮点运算正常情况下应该能精确到小数点后7位。
    2. 性能没你说的那么好,在Pentium4上比使用fdiv指令快约28%,在PentiumM上快约37%。
    3. 比使用divss指令要慢。贡献两个函数:// the FDIV one
    function f_div_fpu(v1, v2: single): single;
    asm
      fld dword ptr [v1]
      fdiv dword ptr [v2]
    end;// for PentiumIII+ processor
    function f_div_sse(v1, v2: single): single;
    asm
      movd xmm1,[v1]
      divss xmm1,[v2]
      movd [v1],xmm1
      fld dword ptr [v1]
    end;
      

  3.   

    个人感觉需要一个新的magic number,从而去掉对原来的sqrt的依赖
    做到跟sqrt一样的效率可惜,至今没人发现它。
      

  4.   

    我是个c++ coder。不会汇编。我认为你只是针对代码做了汇编翻译。
    如果你对c++代码做汇编翻译。执行速度估计会比你的代码还要快。
    原因在于,你绕过了编译器。编译器生成的汇编代码是通用优化。你手工生成的汇编代码是专用优化。所以快。
    并不说明你的算法快。
    算法快必定有算法快的原因与证明推理。
      

  5.   

    又发现了另一件有趣的事情,在Intel Core Duo cpu上我的函数大约快50%,可是在Intel P4上我的函数竟然快了220%
    ///
    p4流水线超长。主频超高。能力超低。因为它只对简单指今操行速度快。复杂指今就over了。
    现在先进的cpu都是擅长处理复杂指今。