【问题标题】:Count digits of number using inline assembly [closed]使用内联汇编计算数字的位数[关闭]
【发布时间】:2015-07-12 21:20:17
【问题描述】:

如何使用 Delphi 的内联汇编器获取数字的位数?

例如:

13452     should return 5
1344      should return 4
9721343   should return 7

我的尝试是这样的:

function CalculateLength(Number : integer) : Integer;
begin
asm
  PUSH Length(Number)
  MOV @Result, EAX
end;
end;

【问题讨论】:

  • 我投票结束这个问题,因为它只是在询问代码。
  • 不,我已经添加了我的尝试。我不仅要代码,还要寻求帮助。
  • 您在寻找什么?您的问题是否完全指定?如何迎合负值?为什么需要使用asm?你对asm一无所知吗?你有性能限制吗?这些是什么?您将如何处理函数的输出?您希望我们编写代码,还是热衷于学习?
  • 先用Delphi写。然后看反汇编(debug),看看生成了什么样的代码。尝试了解如何自己编写。
  • asm 的问题包括: 1. 使用内联到 Pascal 中的 asm。那很危险。使用纯 asm 函数。 2. asm中变量名的使用。这很脆弱,因为它隐藏了寄存器的使用。 3. 推入堆栈,但不弹出。 4. 一厢情愿地认为Length(...) 会神奇地满足您的需求。这表明你的理解与现实之间存在巨大的脱节。 5.莫名其妙的MOV语句。基本上,你没有足够的基础来有任何成功的希望。这听起来可能很苛刻,但我正在努力提供帮助。你需要打好基础。

标签: delphi assembly inline-assembly


【解决方案1】:

这是一种避免除法的更快算法:

function CountDigits(anInt: Cardinal): Cardinal; inline;
var 
  cmp: Cardinal;
begin
  cmp := 10;
  Result := 1;
  while (Result < 10) and (cmp <= anInt) do
  begin
    cmp := cmp*10;
    Inc(Result);
  end;
end;

function CountDigitsAsm(anInt: Cardinal): Cardinal;
asm
   mov ecx,$a          // cmp := 10;
   mov edx,$1          // Result := 1;
   jmp @loop2
   cmp eax,edx         // while cmp <= anInt do
   jb @done
@loop1:
   add ecx,ecx         // cmp := cmp*10;
   lea ecx,[ecx+ecx*4]
   inc edx             // Inc(Result);
@loop2:
   cmp edx,$0a         // (Result < 10)
   jnb @done
   cmp eax,ecx
   jnb @loop1
@done:
   mov eax,edx
end;

begin
  WriteLn(CountDigitsAsm(10));
  WriteLn(CountDigitsAsm(99));
  WriteLn(CountDigitsAsm(999));
  WriteLn(CountDigitsAsm(9999));
  WriteLn(CountDigitsAsm(99999));
  ReadLn;
end.

请注意,pas 版本可以内联,并且可能比 asm 版本更快。


好的,这是避免任何乘法的 lut(查找表)解决方案:

function CountDigitsLUT(anInt: Cardinal): Cardinal; inline;
const
  lut: array[1..10] of cardinal =
    (9,
     99,
     999,
     9999,
     99999,
     999999,
     9999999,
     99999999,
     999999999,
     $FFFFFFFF);
begin
  Result := 1;
  while anInt > lut[Result] do
    Inc(Result);
end;

还有一个展开的版本:

function CountDigitsUnrolled(anInt: Cardinal): Cardinal; inline;
begin
  if (anInt < 10) then Result := 1 else
  if (anInt < 100) then Result := 2 else
  if (anInt < 1000) then Result := 3 else
  if (anInt < 10000) then Result := 4 else
  if (anInt < 100000) then Result := 5 else
  if (anInt < 1000000) then Result := 6 else
  if (anInt < 10000000) then Result := 7 else
  if (anInt < 100000000) then Result := 8 else
  if (anInt < 1000000000) then Result := 9 else
    Result := 10;
end;

还有@TLama 的案例贡献:

function CountDigitsCase(Value: Cardinal): Cardinal; inline;
begin
  case Value of
    0..9: Result := 1;
    10..99: Result := 2;
    100..999: Result := 3;
    1000..9999: Result := 4;
    10000..99999: Result := 5;
    100000..999999: Result := 6;
    1000000..9999999: Result := 7;
    10000000..99999999: Result := 8;
    100000000..999999999: Result := 9;
  else
    Result := 10;
  end;
end;

为不同的解决方案计时:

Unrolled: 4097 ms
Case:     1444 ms
LUT:      3233 ms
pas:      6199 ms
asm:      6747 ms

测试代码:

  sw := TStopWatch.StartNew;
  for i := 1 to 1000000000 do
    j := CountDigitsXXX(i);
  WriteLn(sw.ElapsedMilliseconds,' ',j);

附录

灵感来自this answer, 这是一个 Delphi 实现,它是一个 O(1) 解决方案:

function OpenBit(AValue: Cardinal): Cardinal; register;
asm // Highest bit set
  BSR EAX, EAX
end;

function CountDigitsO1(value: Cardinal): Cardinal; inline;
const
  Powers: array[0..9] of Cardinal = (
    0, 
    10, 
    100, 
    1000, 
    10000, 
    100000, 
    1000000,
    10000000, 
    100000000, 
    1000000000);
  MaxDigits: array[0..32] of cardinal =
    (1, 1, 1, 2, 2, 2, 3, 3, 3, 4, 4, 4, 4, 5, 5, 5,
     6, 6, 6, 7, 7, 7, 7, 8, 8, 8, 9, 9, 9, 10, 10, 10, 10);
begin
  Result := MaxDigits[OpenBit(value)];
  if (value < Powers[Result-1]) then
    Dec(Result);
end;

CountDigitsCase() 相比,它在找到给定数字的解决方案方面具有更均匀的时间分布。但总体上还是有点慢(在我的机器上)。

Digit Case   O1
------------------  
  1   0.930  2.200  nanoseconds per call
  2   0.922  1.689
  3   0.944  1.500
  4   1.078  1.578
  5   1.878  1.522
  6   1.200  1.667
  7   1.356  1.567
  8   1.356  1.522
  9   1.502  1.664
 10   1.246  1.761

测试代码:

procedure TestXXX(var Distribution: array of Double);
var
  sw: TStopWatch;
  i,j,k,m: Cardinal;
const 
  StartIx: array[0..9] of Cardinal = ( 0,10,100,1000,10000,100000,1000000,
    10000000,100000000,100000000);
  StopIx: array[0..9] of Cardinal = ( 9,99,999,9999,99999,999999,9999999,
    99999999,999999999,$FFFFFFFF);
  Repeats: array[0..9] of Cardinal = (10000000,1000000,100000,10000,1000,100,10,1,1,1);
begin
  for k := 0 to 9 do begin
    sw := TStopWatch.StartNew;
    for m := 1 to Repeats[k] do
     for i := StartIx[k] to StopIx[k] do
      j := CountDigitsXXX(i);
    Distribution[k] := sw.ElapsedMilliseconds*1000000.0/(1.0*Repeats[k]*(StopIx[k]- StartIx[k] + 1)); 
    WriteLn(sw.ElapsedMilliSeconds,' ',j);      
  end;
end;

【讨论】:

  • 问题要求组装。在 Pascal 中回答隐藏了一些汇编问题,例如保留寄存器、处理堆栈帧或调用约定。
  • @rkhb,我的答案包括汇编,但在这种情况下,我建议不要采用这样的解决方案,因为在这种情况下很难击败编译器(内联 asm 是不可能的)。
  • 哇哦!我的case 赢得了这一轮 :) @GJ。对于这个循环测试来说确实如此,但谁知道这个函数使用的数据......出于好奇,我检查了持续返回该展开循环以获取值 1(从第一个代码行返回)需要多长时间。即便如此,case 在我的机器上还是要快一些(旧 i5,应用程序。XE8 32 位、64 位、发布配置、默认设置)。我没有检查生成的代码,但我也希望case 会更慢。无论如何,我不会深入研究这个。这个问题与性能无关......
  • @SilverWarior,是的,这很简单,但性能不太好(并且在 asm 中编写代码很棘手)。
  • 非常好的方法,使用准Ln2和LUT。
【解决方案2】:

我的做法:

function count_of_digits (n:Integer) : Cardinal;    {No problems for `n:Cardinal`}
var cnt : Cardinal;
begin
    cnt := 0;
    repeat
        inc (cnt);
        n := n div 10;
    until (n=0);
    count_of_digits := cnt;
end;

function count_of_digits_asm_signed (n:Integer) : Cardinal;
begin
    asm
        push ebx                { An asm statement must preserve the EDI, ESI, ESP, EBP, and EBX registers }
        xor ecx, ecx
        mov ebx, 10             { Base 10 (decimal), just change it for another base }
        mov eax, n

        @L1:
        add ecx, 1
        cdq                     { Set EDX for `idiv` }
        idiv ebx                { Decimal shift right by one decimal digit }
        test eax, eax
        jne @L1

        mov @result, ecx
        pop ebx
    end;
end;

function count_of_digits_asm_unsigned (n:Cardinal) : Cardinal;
begin
    asm
        push ebx                { An asm statement must preserve the EDI, ESI, ESP, EBP, and EBX registers }
        xor ecx, ecx
        mov ebx, 10             { Base 10 (decimal), just change it for another base }
        mov eax, n

        @L1:
        add ecx, 1
        xor edx, edx            { Set EDX for `div` }
        div ebx                 { Decimal shift right by one decimal digit }
        test eax, eax
        jne @L1

        mov @result, ecx
        pop ebx
    end;
end;

VAR
    i: Integer;
    c1, c2, c3: Cardinal;

BEGIN
    i := 13452;
    c1 := count_of_digits (i);
    c2 := count_of_digits_asm_signed (i);
    c3 := count_of_digits_asm_unsigned (i);
    writeln (i:11, c1:3, c2:3, c3:3);

    i := 1344;
    c1 := count_of_digits (i);
    c2 := count_of_digits_asm_signed (i);
    c3 := count_of_digits_asm_unsigned (i);
    writeln (i:11, c1:3, c2:3, c3:3);

    i := 9721343;
    c1 := count_of_digits (i);
    c2 := count_of_digits_asm_signed (i);
    c3 := count_of_digits_asm_unsigned (i);
    writeln (i:11, c1:3, c2:3, c3:3);

    i := -13452;
    c1 := count_of_digits (i);
    c2 := count_of_digits_asm_signed (i);
    c3 := count_of_digits_asm_unsigned (i);
    writeln (i:11, c1:3, c2:3, c3:3);
END.

【讨论】:

  • 那非常昂贵,执行那些可以使用比较的除法。将 asm 内联到 Pascal 函数中是不好的做法。而且您还通过使用 Pascal 变量名称来自找麻烦。将结果放入 eax 而不是引用名称。
  • 类似这样的东西:pastebin.com/L1heePPt
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-06-09
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多