【问题标题】:Why does Delphi insert nop's in the middle of nowhere?为什么Delphi会在不知名的地方插入nop?
【发布时间】:2026-02-15 05:00:01
【问题描述】:

以下代码:

while Assigned(p) do begin
  np:= p^.next;
  h:= leaf_hash(p^.data);   <<-- inline routine
  h:= h mod nhashprime;
  p^.next:= nhashtab[h]; 
  nhashtab[h]:= p;
  p:= np;
end; { while }

生成以下程序集:

hlife.pas.605: h:= leaf_hash(p^.data);
00000000005D4602 498B4018         mov rax,[r8+$18]
00000000005D4606 48C1E830         shr rax,$30
00000000005D460A 498B5018         mov rdx,[r8+$18]
00000000005D460E 48C1EA20         shr rdx,$20
00000000005D4612 81E2FFFF0000     and edx,$0000ffff
00000000005D4618 4D8B5818         mov r11,[r8+$18]
00000000005D461C 49C1EB10         shr r11,$10
00000000005D4620 4181E3FFFF0000   and r11d,$0000ffff
00000000005D4627 418B7018         mov esi,[r8+$18]
00000000005D462B 81E6FFFF0000     and esi,$0000ffff
00000000005D4631 488D34F6         lea rsi,[rsi+rsi*8]
00000000005D4635 4403DE           add r11d,esi
00000000005D4638 4F8D1CDB         lea r11,[r11+r11*8]
00000000005D463C 4103D3           add edx,r11d
00000000005D463F 488D14D2         lea rdx,[rdx+rdx*8]
00000000005D4643 03C2             add eax,edx
hlife.pas.606: h:= h mod nhashprime;
00000000005D4645 8BC0             mov eax,eax   <<--- Why is there a NOP here?
00000000005D4647 4C63DB           movsxd r11,rbx
00000000005D464A 4899             cwd
00000000005D464C 49F7FB           idiv r11
00000000005D464F 488BC2           mov rax,rdx
hlife.pas.607: p^.next:= nhashtab[h];
00000000005D4652 488B5538         mov rdx,[rbp+$38]

Delphi 在nhashtab[h]:= p; 行之前插入一个NOP。 如果 leaf_hash 函数是一个普通函数,那将是有意义的。
(不是真的,因为 RET 仍会返回到 [5D4645] 执行 nop)时间>
但是现在这不是一个跳转目标。

所以我(只是)很好奇,为什么会这样?

[编辑]:SSCCE
好的,我有一个 SSCCE {它不是很短,但它必须这样做。

注意编译器设置(Debug + Win64)

unit Unit16;

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants, System.Classes, Vcl.Graphics,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Vcl.StdCtrls;

type
  pnode = ^node;
  tflavour = (tnode, tleaf, tleaf64);

  node = record
    case flavour: tflavour of
      tnode: (next: pnode; (* hash link *)
          nw, ne, sw, se: pnode; (* constant; nw not= 0 means nonleaf *)
          res: pnode); (* cache *)
      tleaf: (next1: pnode; (* hash link *)
          isnode: pnode; (* must always be zero for leaves *)
          nw1, ne1, sw1, se1: word; (* constant *)
          res1, res2: word; (* constant *)
        );
      tleaf64: (next2: pnode; (* hash link *)
          isnode1: pnode; (* must always be zero for leaves *)
          data: Uint64; (* constant *)
          res1a, res2a: word; (* constant *)
        )
  end;

  ppnode = array of pnode;

  THashBox = class(TPersistent)
  strict private
    leafhashpop: integer;
    leafhashlimit: integer;
    leafhashprime: integer;
    leafhashtab: ppnode;
    nodehashpop: integer;
    nodehashlimit: integer;
    nodehashprime: integer;
    nodehashtab: ppnode;
  private
    TotalTime, Occurrences: Uint64;
    StartTime, EndTime: Uint64;
    procedure resize_leaves();
  public
    constructor Create;
  end;

  TForm16 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    HashBox: THashBox;
  public
  end;

var
  Form16: TForm16;

implementation

{$R *.dfm}

const
  maxmem = 2000*1000*1000;   {2GB}

var
  alloced: cardinal;

function rdtsc: int64; assembler;
asm
  { xor eax,eax;
  push rbx
  cpuid
  pop rbx }
  rdtsc
end;

function node_hash(n: pnode): cardinal; { inline; } assembler; overload;
// var
// a: pnativeint;
  // begin
  // Result:= nativeint(n^.se) + 3 * (nativeint(n^.sw) + 3 * (nativeint(n^.ne) + 3 * nativeint(n^.nw) + 3));
asm
  mov eax,[rcx+node.nw]
  lea eax,[eax+eax*2+3]
  add eax,[rcx+node.ne]
  lea eax,[eax+eax*2]
  add eax,[rcx+node.sw]
  lea eax,[eax+eax*2]
  add eax,[rcx+node.se]
end;

function leaf_hash(a, b, c, d: cardinal): cardinal; inline; overload;
begin
  Result:= (d + 9 * (c + 9 * (b + 9 * a)))
end;

function leaf_hash(data: Uint64): cardinal; inline; overload;
begin
  // Result:= d + 9 * (c + 9 * (b + 9 * a));
  Result:= ((data shr 48) + 9 * (((data shr 32) and $FFFF) + 9 * (((data shr 16) and $FFFF) + 9 * (data and $FFFF))));
  Inc(Result);
end;

procedure TForm16.Button1Click(Sender: TObject);
begin
  HashBox:= THashBox.Create;
  Hashbox.resize_leaves;
end;

function nextprime(old: integer): integer;
begin
  Result:= 1009;
end;

constructor THashBox.Create;
begin
  leafhashprime:= 7;
  SetLength(leafhashtab, leafhashprime);
end;

procedure THashBox.resize_leaves();
var
  i, i1, i2: integer;
  nhashprime: Cardinal;
  p: pnode;
  nhashtab: ppnode;
  np: pnode;
  h: Integer;
  n, n2: integer;
  diff1, diff2: integer;
begin
  nhashprime:= nextprime(4 * leafhashprime);
  if (nhashprime * sizeof(pnode) > maxmem - alloced) then begin
    leafhashlimit:= 2000 * 1000 * 1000;
    exit;
  end;
  (*
    *   Don't let the hash table buckets take more than 4% of the
    *   memory.  If we're starting to strain memory, let the buckets
    *   fill up a bit more.
  *)
  if (nhashprime > maxmem div 100) then begin
    nhashprime:= nextprime(maxmem div 100);
    if (nhashprime = leafhashprime) then begin
      leafhashlimit:= 2000 * 1000 * 1000;
      exit;
    end;
  end;
  SetLength(nhashtab, nhashprime); //make a new table, do not resize the existing one.
  alloced:= alloced + sizeof(pnode) * (nhashprime - leafhashprime);

  diff1:= maxint;
  for i1:= 0 to 100 do begin
    n:= 0;
    StartTime:= rdtsc;
    for i:= 0 to leafhashprime - 1 do begin
      p:= leafhashtab[i];
      if Assigned(p) then begin
        h:= node_hash(p);
        h:= h mod nhashprime;
        inc(n, h);
      end;
    end;
    EndTime:= rdtsc;
    if ((EndTime - StartTime) < diff1) then diff1:= (EndTime - StartTime);

  end;

  diff2:= maxint;
  for i1:= 0 to 100 do begin
    n2:= 0;
    StartTime:= rdtsc;
    for i:= 0 to leafhashprime - 1 do begin
      p:= leafhashtab[i];
      if Assigned(p) then begin
        inc(n2);
      end;
    end;
    EndTime:= rdtsc;
    if (endtime - starttime) < diff2 then diff2:= endtime - starttime;
  end;

  TotalTime:= diff1 - diff2;
  if n <> n2 then Occurrences:= nhashprime;

  for i:= 0 to leafhashprime - 1 do begin
    // for (p=hashtab[i]; p;) {
    p:= leafhashtab[i];
    while Assigned(p) do begin    <<--- put a breakpoint here
      np:= p^.next;
      h:= leaf_hash(p^.data);
      h:= h mod nhashprime;
      p^.next:= nhashtab[h];
      nhashtab[h]:= p;
      p:= np;
    end; { while }
  end; { for i }
  // free(hashtab);
  leafhashtab:= nhashtab;
  leafhashprime:= nhashprime;
  leafhashlimit:= leafhashprime;
end;

end.

你会看到这个反汇编:

Unit16.pas.196: h:= h mod nhashprime;
000000000059CE4B 4863C0           movsxd rax,rax
000000000059CE4E 448B5528         mov r10d,[rbp+$28]
000000000059CE52 458BD2           mov r10d,r10d     <<--- weird NOP here 
000000000059CE55 4899             cwd
000000000059CE57 49F7FA           idiv r10
000000000059CE5A 488BC2           mov rax,rdx
Unit16.pas.197: p^.next:= nhashtab[h];
000000000059CE5D 488B5538         mov rdx,[rbp+$38]

【问题讨论】:

  • 我猜可能是为了调试?老实说,我不知道
  • 我最好的猜测是某种形式的优化以在特定边界上对齐代码字节。
  • 这个问题非常需要SSCCE。没有它,我们就无法探测编译器。我想这样做,并使用不同版本的编译器。但我不能。
  • @DavidHeffernan OK SSCCE 上线了,时间不是很短,但它可以胜任。
  • 当然循环内的代码长度需要是16bytes的倍数。所以 no-ops 是可以的,因为它只是它们的位置,但由于某种原因将它们放在最后可能会很尴尬,或者它可能工作正常,因为 nop 可以破坏依赖链,因此不会花费循环执行。请记住,对于 XE2,x64 编译器有一个全新的代码生成引擎,所以我们从 D3-DXE 中知道的相同旧相同旧代码生成已经不存在了。

标签: delphi assembly delphi-xe2


【解决方案1】:

答案是

MOV EAX,EAX

不是无操作

在 x64 上,操作 64 位寄存器的低 32 位会将高 32 位归零。
所以上面的指令真的应该读成:

MOVZX RAX,EAX 

根据 AMD

32 位运算的结果隐式零扩展为 64 位值。

【讨论】:

    【解决方案2】:

    恕我直言,这不是用于对齐的nop,但在我看来,这就像未优化的生成代码,以及您自己的变量的错误签名。

    h:= h mod nhashprime;
    

    可分为:

    mov eax,eax       new h = eax, old h = eax  // which does not mean anything
    movsxd r11,rbx    convert with sign nhashprime stored in rbx into temp registry r11
    cwd               signed rax into rdx:rax
    idiv r11          signed divide rdx:rax by r11 -> rax=quotient, rdx=remainder
    mov rax,rdx       store remainder rdx into rax
    

    您是否尝试过启用代码生成优化?我想它会修复mov eax,eax的内容。

    但是您的原始代码也经过了子优化。 你应该在你的情况下使用无符号算术。

    而且,您最好使用nhashprime 的二次幂,计算一个简单的and 二元运算而不是慢除法:

    var h, nhashprimeMask: cardinal; // use UNSIGNED arithmetic here!
    
    // here nhashprime is a POWER OF TWO (128,256,512,1024,2048...)
    nhashprimeMask := nhashprime-1; // compute the mask
    
    while Assigned(p) do begin
      np:= p^.next;
      h:= leaf_hash(p^.data) and nhashprimeMask;
      p^.next:= nhashtab[h]; 
      nhashtab[h]:= p;
      p:= np;
    end; { while }
    

    这段代码会快得多,而且应该编译得更好。

    【讨论】:

    • 哈希表的素数大小以外的任何内容都会导致更多的哈希冲突。现在我正好处于理论上的最佳状态。如果我用 2 的幂替换素数,则哈希冲突会大幅上升(从半满时的 18% 冲突到半满时的 81%)
    • 是的,优化已开启
    • 将整数更正为基数时,问题确实消失了。当关闭优化时问题也消失了。
    • @Johan 您必须正确计算两个值的幂。例如,看看它是如何在 RTL 代码(例如泛型集合)或 mORMot 中完成的。在实践中,它并不像你所说的那么糟糕,or your hash algorithm is to be re-considered(例如,我们使用 crc32 和预先分配的表取得了巨大的成功)。 AFAIK 现代 CPU 足够聪明,可以在执行前将 x64 asm 转换为内部微操作时 忽略 NOP。唯一的问题可能是缓存未命中,在您的情况下不是大问题。
    • 我去看看mORMot。
    【解决方案3】:

    这是一种对齐代码的优化,尤其是在循环中,以避免缓存行停顿等。

    【讨论】:

    • 我希望这些对齐 NOP 位于循环的末尾或开头,而不是中间的某个地方,除非有什么我没有到达这里。把它们放在没有分支目标的中间有什么目的?
    • 你会解释这如何导致优化吗?因为它看起来肯定不会。
    • 我同意大卫的观点。在循环标签之前进行对齐,对齐到 4/8 字节边界。这里没有对齐,只是优化错误。