技術記事

32ビットDelphiからDCC64への_ftolインラインアセンブリの移植

32ビットDelphiの元々の _ftol イディオムは、気の利いたワンライナーのように見えます。つまり、インラインアセンブリに移行してx87 FPUのコントロールワードを操作し、FPUスタック上の値を切り捨てて、結果をポップするPascal関数ラッパーです。これはDCC32の下で長い間問題なくビルドされてきたため、誰にも疑問を持たれることなく、非常に多くの古いグラフィックスやPDFユニットに組み込まれることになりました

ビルドターゲットを64ビットに切り替えると、コンパイラは E1025 サポートされていない言語機能: 'ASM' を出して終了します。このエラーは互換性の警告ではありません。以前のアセンブリがどれほどうまく機能していたかに関係なく、DCC64がルーチンを一切コンパイルしないことを意味します

32ビットのオリジナルは、通常次のようなものでした:

function _ftol(f: Double): Integer; cdecl;
begin
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
end;

Pascalの begin...end 本体内にあるその asm ブロックこそ、DCC64が拒否するものです。2つのコンパイラには、アセンブリが許可される場所について異なるルールがあり、その境界が重要なのです

DCC64が異なる線を引く理由

DCC32は、通常のPascalルーチン内でのインラインアセンブリを許可します。コンパイラは32ビットの呼び出し規約を認識しており、ローカル変数やパラメーターが存在する場所を推論できるため、名前によってスタックフレームにアクセスするアセンブリフラグメントを許容します。DCC64はより厳格な立場をとります。アセンブリは専用のアセンブラ関数、つまり本体全体がアセンブリであり、呼び出し規約が明示的に処理される関数内に存在する必要があります。Pascalとasmの混在は一切サポートされていません

根本的な理由はアーキテクチャ上のものです。64ビットWindowsの呼び出し規約(Microsoft ABI)では、最初の4つのパラメーターは、整数型の場合は RCXRDXR8R9 に、浮動小数点の場合は XMM0 から XMM3 に到着します。通常のパラメーターの受け渡しにx87 FPUは関与しません。技術的にはx87は利用可能ですが、ABIは引数の転送にそれを使用しません。値が「FPUスタック上」にあると想定するアセンブリは、64ビットABIが決して作成しない状態について推論していることになります

したがって、古いフラグメントには構文上の問題があるだけではありません。DCC64がそれを受け入れたとしても、レジスタの仮定が間違っているのです

適切な64ビットアセンブラバージョンの作成

バイナリの互換性のために、どうしても cdecl 規約で _ftol シンボルをエクスポートする必要がある場合、関数は純粋なアセンブラルーチンとして記述する必要があります。64ビットABIでは、Double パラメーターは XMM0 に到着し、整数の結果は戻り時に RAX に格納されている必要があります。.NOFRAME ディレクティブは、ルーチンが独自のスタックを管理することをDCC64に伝えます。これは、このような短いリーフ関数に適しています:

function _ftol: Integer; cdecl;
// Double value expected in XMM0 per 64-bit ABI
asm
  .NOFRAME
  cvttsd2si  rax, xmm0   // truncate-to-integer, result in rax
end;

CVTTSD2SI は、倍精度浮動小数点をゼロに向けて切り捨てて符号付き整数に変換するためのSSE2命令であり、これはまさに _ftol が行うべきことです。これは単一の命令であり、ABIが残した場所からパラメーターを直接受け取り、ABIが期待する場所に結果を配置します。FPUコントロールワードをやりくりする必要はありません

入力が32ビット符号付き整数の範囲を超える場合、CVTTSD2SI は整数の不定値($80000000)を返すことに注意してください。これは、範囲外の入力に対するx87 fistp と同じ動作です。呼び出し側がそのような値を生成する可能性があるかどうかは、移行の完了を宣言する前に確認する価値があります

Truncがより良い答えである場合

上記のアセンブラバージョンは、実際のバイナリ互換性の要件がある場合にのみ作成する価値があります。つまり、外部の呼び出し側が特定の呼び出し規約で _ftol シンボルを期待しており、それらの呼び出し側を変更できない場合です。そのような状況はまれです。ほとんどの場合、_ftol は同じユニット内でのみ使用されるプライベートなヘルパーであり、その名前や規約に対する外部依存関係はまったくありません

その場合は、プレーンなPascalに置き換えます:

function _ftol(f: Double): Integer; cdecl;
begin
  Result := Trunc(f);
end;

Trunc はゼロに向けて切り捨てます。これは、x87コントロールワードを切り捨てモードに設定して _ftol が行っていたことと一致します。DCC32とDCC64で変更なしでコンパイルされます。コンパイラはターゲットごとに適切な命令を生成します。x64では、いずれにせよ通常は手書きバージョンと同じ命令である CVTTSD2SI を出力します。同一の動作が得られ、プラットフォームの条件文もなく、保守するアセンブリもありません

確認する価値のある1つのセマンティックな違いがあります。入力がNaNまたは無限大の場合、Delphiのデフォルト設定では TruncEInvalidOp 例外を発生させます。元のコードのx87 fistp は、何も発生させずに単にビットパターンを書き込んでいました。コードがこの関数に異常な浮動小数点値を供給しており、古い動作が静かであった場合は、Trunc を呼び出す前に MathIsNaNIsInfinite で保護してください

両方のターゲットがアクティブなままの場合の条件付きコンパイル

一部のプロジェクトでは、32ビットと64ビットの両方のバイナリを出荷し続ける必要があります。32ビット用に元のアセンブラバージョンを保持し、64ビット用に新しい実装を提供する必要がある場合は、CPUX64 条件文を使用します:

function _ftol(f: Double): Integer; cdecl;
begin
{$IFDEF CPUX64}
  Result := Trunc(f);
{$ELSE}
  // 32-bit path: DCC32 accepts inline asm
  asm
    lea   eax, f
    fstp  qword ptr [eax]
  end;
  Result := Trunc(f);
{$ENDIF}
end;

これが最小限の機械的な修正であり、一時的なものとして扱う価値があります。唯一の目的が浮動小数点から整数への切り捨てであるヘルパーに、アーキテクチャ固有のアセンブリを抱えるコードベースは、不要な負債を抱え込んでいることになります。古い実装のFPUの副作用に依存するものがないことを確認できれば、32ビットの分岐は完全になくすことができます

関数が複数のユニットで使用されるコンポーネント内にある場合は、コードベース全体で _ftol を検索してから、移行方法を決定してください。その名前のシンボルは複数の場所で宣言される可能性があります。リンカーは1つを選択し、他を暗黙のうちに無視します。つまり、1つのコピーを修正しても、手つかずの別のコピーにリンクされる可能性があるということです