3

バイトを格納するレコードを使用して、プログラムの他の部分にあるオブジェクトのフィールドにアクセスし、同じ名前の関数を使用して別のレコードのフィールドにアクセスするエレガントな方法を見つけようとしています。レコードのフィールド。

TAilmentP = Record // actually a number but acts like a pointer
private
  Ordinal: Byte;
public
  function Name: String; inline;
  function Description: String; inline;
  class operator Implicit (const Number: Byte): TAilmentP; inline;
End;

 TSkill = Class
   Name: String;
   Power: Word;
   Ailment: TAilmentP;
 End;

class operator TAilmentP.Implicit (const Number: Byte): TAilmentP;
begin
  Result.Ordinal := Number;
  ShowMessage (IntToStr (Integer (@Result))); // for release builds
end;

function StrToAilment (const S: String): TAilmentP; // inside same unit
var i: Byte;
begin
  for i := 0 to Length (Ailments) - 1 do
    if Ailments [i].Name = S then
    begin
      ShowMessage (IntToStr (Integer (@Result))); // for release builds
      Result := i; // uses the Implicit operator
      Exit;
    end;
  raise Exception.Create ('"' + S + '" is not a valid Ailment"');
end;

ここで、変換演算子をオーバーロードして作業を楽にしようとしていたので、バイトを TAilmentP オブジェクトに割り当てようとすると、それが Ordinal フィールドに割り当てられます。ただし、私が確認したように、暗黙の「演算子」への呼び出しは戻り値の新しい TAilmentP オブジェクトを作成し、そのビジネスを行い、値を返し、アドレスが異なるため、それを呼び出したオブジェクトにバイトごとのコピーを作成します。

正直なところ、私のコードはこのメソッドをかなり頻繁に呼び出しており、オブジェクトの Ordinal フィールドに値を直接代入するよりも遅いようです。

ANYメソッド/関数を使用して、プログラムに実際に値をフィールドに直接割り当てる方法はありますか? インライン化しても機能しないようです。オブジェクト自体ではなく、(レコード) 変数への参照を返す方法はありますか? 最後に (トピックから少し外れて申し訳ありません)、なぜ演算子のオーバーロードは静的関数を介して行われるのでしょうか? それらを逆参照せずにオブジェクトフィールドにアクセスできるので、それらをインスタンスメソッドにすると高速になりますか? これは、ここと私のコードの他の部分で本当に便利です。

[編集] これは、すべての最適化が有効で、デバッグ機能がない (ブレークポイントの「デバッグ情報」でさえない) Implicit オペレーターのアセンブラー コードです。

add al, [eax] /* function entry */
push ecx
mov [esp], al /* copies Byte parameter to memory */
mov eax, [esp] /* copies stored Byte back to register; function exit */
pop edx
ret

さらにおもしろいのは、次の関数の起動時に mov eax, eax 命令があることです。今では本当に便利に見えます。:P そうそう、私の Implicit 演算子もインライン化されませんでした。

[esp] は Result 変数であると確信しています。割り当て先とは異なるアドレスを持っているからです。最適化をオフにすると、[esp] は [ebp-$01] (割り当て先) と [ebp-$02] (Byte パラメーター) に置き換えられ、[ebp-$02] を AL に移動する命令がもう 1 つ追加されます (その後、 [ebp-$01] に配置します)、冗長な mov 命令は [epb-$02] にまだあります。

何か間違ったことをしていますか、それとも Delphi には戻り値の最適化がありませんか?

4

3 に答える 3

3

レジスタに収まる戻り値の型 (レコードであっても) は、レジスタを介して返されます。参照によって関数に渡される「出力」パラメーターに内部的に変換されるのは、より大きな型のみです。

レコードのサイズは 1 です。レコードのコピーの作成は、通常の のコピーの作成と同じくらい高速Byteです。

Result変数のアドレスを監視するために追加したコードは、実際にはオプティマイザに悪影響を及ぼしています。変数のアドレスを要求しない場合、コンパイラは変数にメモリを割り当てる必要はありません。変数はレジスタにのみ存在できます。アドレスを要求すると、コンパイラはスタック メモリを割り当てて、アドレスを提供する必要があります。

「リリース モード」コードを取り除き、代わりに CPU ウィンドウでコンパイラの作業を観察します。レコードが主にレジスターにどのように存在するかを観察できるはずです。Implicit入力レジスターと出力レジスターは両方とも EAX でなければならないため、オペレーターはノーオペレーションにまでコンパイルされる可能性さえあります。


演算子がインスタンス メソッドであるか静的であるかは、特にパフォーマンスの点で大きな違いはありません。インスタンス メソッドは、呼び出されたインスタンスへの参照を引き続き受け取ります。参照に選択した名前があるかどうか、またはそれが呼び出されSelfて暗黙的に渡されるかどうかだけの問題です。「Self」と書く必要はありませんが。フィールド アクセスの前に、静的演算子メソッドのパラメーターと同様に、Self 変数を逆参照する必要があります。

他の言語での最適化について私が言うのは、戻り値の最適化という名前の用語、またはその略語 NRVO を調べる必要があるということだけです。以前、Stack Overflow で取り上げられました。インライン化とは関係ありません。

于 2010-01-19T18:41:37.913 に答える
1

Delphi は、ポインタを使用して戻り代入を最適化することになっています。これは、C++ やその他の OOP コンパイル言語にも当てはまります。演算子のオーバーロードが導入される前に Pascal を書くのをやめたので、私の知識は少し古くなっています。以下は私が試みるものです:

私が考えているのはこれです...ヒープ上にオブジェクトを作成し(Newを使用)、「暗黙的」メソッドからポインターを返すことができますか?これにより、不要なオーバーヘッドを回避できますが、戻り値をポインターとして処理する必要があります。メソッドをオーバーロードしてポインター型を処理しますか?

組み込みの演算子のオーバーロードでこれを実行できるかどうかはわかりません。前述したように、オーバーロードは、私が Pascal で 10 年近く望んでいた機能であり、一度も手を加えることができませんでした。やってみる価値はあると思います。エレガントなタイプのキャストの夢を殺さなければならないことを受け入れる必要があるかもしれません。

インライン化にはいくつかの注意事項があります。デバッグ ビルドではヒントが (デフォルトで) 無効になっていることは既にご存じでしょう。プロファイリング/ベンチマークを実行したり、ビルド設定を変更したりするには、リリース モードにする必要があります。まだリリース モードに移行していない (またはビルド設定を変更していない) 場合は、インライン ヒントが無視されている可能性があります。

const を使用して、コンパイラにさらに最適化するように指示してください。あなたのケースではうまくいかなくても、始めるのに最適な方法です。変更してはいけないものをマークすることで、あらゆる種類の災害を防ぐことができます...さらに、コンパイラに積極的に最適化する機会を与えます。

Delphi がクロスユニットのインライン展開を許可しているかどうか知りたいのですが、私は単純に許可していません。多くの C++ コンパイラは、コードをヘッダーに配置しない限り、同じソース コード ファイル内でインライン化するだけです (Pascal ではヘッダーに相関関係はありません)。1、2 回検索する価値があります。可能であれば、インライン化された関数/メソッドを呼び出し元に対してローカルにするようにしてください。それ以上ではないにしても、少なくともコンパイル時間は短縮されます。

すべてのアイデアから。うまくいけば、この蛇行が役に立ちます。

于 2010-01-19T02:15:00.913 に答える
0

考えてみると、戻り値を別のメモリ空間に置き、割り当てられているメモリ空間にコピーし直すことが絶対に必要なのかもしれません。

たとえば、バイト値を持つ TAilmentP パラメーターを受け入れる関数を呼び出すなど、戻り値の割り当てを解除する必要がある場合を考えています...関数のパラメーターに直接割り当てることはできないと思いますまだ作成されていないため、それを修正すると、アセンブラーで関数呼び出しを生成する通常の確立された方法が壊れます (つまり、作成される前にパラメーターのフィールドにアクセスしようとすることはできないため、そのパラメーターを作成する必要があります)。その前に、コンストラクターの外側に割り当てる必要があるものをそれに割り当ててから、アセンブラーで関数を呼び出します)。

これは、他の演算子 (式を評価できるため、一時オブジェクトを作成する必要がある) に特に当てはまり、明らかですが、他の言語 (C++ など) の代入演算子と似ていると思うので、これはあまり重要ではありません。これはインスタンス メンバーにすることができます) が、実際にはそれ以上のものです。これはコンストラクタでもあります。例えば

procedure ShowAilmentName (Ailment: TAilmentP);
begin
  ShowMessage (Ailment.Name);
end;

[...]
begin
ShowAilmentName (5);
end.

はい、暗黙の演算子もそれを行うことができます。これは非常にクールです。:D この場合、5 は、他のバイトと同様に、(そのバイトに基づいて新しい TAilmentP オブジェクトを作成する場合のように) TAilmentP に変換されると考えています。暗黙の演算子を指定すると、オブジェクトはバイト単位でコピーされますを Ailment パラメータに入力すると、関数本体が入力され、そのジョブが実行され、戻り時に変換から取得された一時的な TAilmentP オブジェクトが破棄されます。これは、Ailment が const である場合、参照である必要があり、定数でもある必要があるため、さらに明白です (関数が呼び出された後に変更する必要はありません)。

C++ では、代入演算子は関数呼び出しとは関係ありません。代わりに、Byte パラメーターを受け入れる TAilmentP のコンストラクターを使用することもできます。Delphiでも同じことができ、暗黙の演算子よりも優先されると思いますが、C++はサポートしていませんが、Delphiがサポートしているのは、プリミティブ型(Byte、Integerなど)へのダウンコンバージョンです。クラス演算子を使用してオーバーロードされます。したがって、「procedure ShowAilmentName (Number: Byte);」のようなプロシージャ C++ では「ShowAilmentName (SomeAilment)」のような呼び出しを受け入れることはできませんが、Delphi では可能です。

したがって、これは Implicit 演算子がコンストラクターのようなものであることの副作用だと思います。レコードはプロトタイプを持つことができないため、これが必要です (したがって、コンストラクターを使用するだけでは、2 つのレコード間で一方と他方の両方を変換することはできませんでした)。 . 他の誰かがこれが原因かもしれないと思いますか?

于 2010-01-19T17:11:48.430 に答える