var
X
=
0
if
:
set
X
=
42
# これは、次の行により
# ロールバックされます
X
>
100
# これは失敗し、ロールバックが発生します
X
=
0
# X はロールバックにより 0 に戻ります
set
処理だけでなく、どのエフェクトにも該当します。たとえば、代わりに C++ 関数を呼び出し、C++ を使用して可変変数を変更 (つまり、ポインタに格納) してから、他の C++ 関数を使用して、その可変変数を読み取ることができます。SetSomething
(
0
)
# C++ で実装され、C++ 変数を
# 設定します
if
:
SetSomething
(
42
)
# これは、次の行により
# ロールバックされます
GetSomething
()
>
100
# これは失敗して、ロールバックが発生し
GetSomething
()
=
0
# ロールバックにより 0 に戻ります
if
文の条件などの Verse の失敗コンテキストはネスティングされているトランザクションであり、条件が成功の場合はコミットし、失敗の場合は、中断する。条件式には、エフェクトを含むことができます。条件が失敗した場合、そのエフェクトは一切発生しなかったかのように処理されます。<no_rollback>
エフェクトで対処していますが、問題が修正され次第、<no_rollback> エフェクトは非推奨となります。<no_rollback>
エフェクト) うえに、エラーが発生しやすく、削除されたエフェクトに対してアンドゥ ハンドラが登録された場合など、デバッグが困難なクラッシュにつながります。void
Foo
()
{
TArray
<int> Array
=
{
1
,
2
,
3
};
AutoRTFM::
Transact
(
[
&
] ()
{
Array
.
Push
(
42
);
//
配列は {1, 2, 3, 42} ここです。
AutoRTFM::
Abort
();
});
//
配列は、中断により {1, 2, 3} ここです。
}
TArray
に変更を加えることなく動作します。これは、TArray
が Push
の内部でバッキング ストアを再割り当てする必要がある場合でも機能します。TArray
は、この点では特殊ではありません。AutoRTFM は、std::vector
/std::map
だけでなく、Unreal Engine や C++ の標準ライブラリの他のデータ構造や関数の多くに対してこれを行うことができます。これを実現するために必要な唯一の変更は、AutoRTFM と低レベルのメモリ アロケータおよびガベージ コレクタ (GC) との統合です。このセクションで説明しますが、これらも小規模な変更です (これらは純粋に加算的なわずか数行の変更です。Epic Games では、Unreal Engine の多数の malloc、Unreal Engine の GC、およびシステムの malloc/new のすべてにこれらを追加しました)。さらに、AutoRTFM は、トランザクション外 (つまり、Transact
呼び出しの動的スコープの外側) で実行されるそれらのコード パスの「パフォーマンスを低下させることなく」これを実現します。重要なのは、(TArray
をはじめとするその他多くの) コードパスがトランザクション内から使用される場合でも、トランザクション外での使用にコストが発生しないということです。コードがトランザクション内部で実行されている場合、オーバーヘッドは高い (一部の内部ベンチマークで 4 倍が確認されている) ものの、フォートナイト サーバーのティック レートが損なわれるほどではありません。AutoRTFMPass
の役割は、Verse のトランザクション セマンティクスを遵守できるように、すべてのコードをインスツルメンテーションすることです。ただし、フォートナイトの同じコードが、スタック上に Verse のコードがなく Verse のセマンティクスが不要な場所と、スタック上に Verse がありロールバックが可能な場所で使用される可能性があります。bCurrentThreadIsInTransaction
ビットは、プログラム カウンタにエンコードされます。コンパイラの観点では、これはシンプルです。つまり、コードをすべてクローンし、クローンした関数に修飾名を付け、クローンのみをインスツルメンテーションするだけです。元のコードにはインスツルメンテーションは行われず、ログ記録やその他の特別な処理は行いません。そのため、そのコードを実行させるために、コストは一切発生しません。クローンされたコードはインスツルメンテーションを取得し、トランザクション内であることを確実に認識します。これにより、代替呼び出しルールを使用するなど、VM のような手法を実行することもできます。AutoRTFMPass
は、LLVM パイプラインのサニタイザーと同じ時点で実行されます。つまり、IR の最適化がすべて完了した後、バックエンドにコードを送信する直前に実行されます。これは重要な意味を持ちます。たとえば、インスツルメンテーションされたクローン内でも、インスツルメンテーションを取得するロードとストアは、最適化後に存続しているロードとストアのみです。つまり、C++ での非エスケープ ローカル変数への割り当ては抽象的には「ストア」であるものの (clang では clang 用に最適化されていない LLVM IR を作成するときに、この方法で表現する)、AutoRTFMPass
が処理を行う時点では、ストアは完全に存在しなくなっています。ほとんどの場合、ストアは SSA データフローに置き換えられます。これは AutoRTFM で処理する必要はありません。また、Epic Games では、既存の LLVM の静的分析に基づくいくつかの細かい最適化を実装し、その必要がないことを証明できる場合には、ストアのインスツルメンテーションを回避しています。AutoRTFM::Transact
です。この関数をラムダを使用して呼び出すと、ランタイムはログ データ構造とその他のトランザクション状態を含むトランザクション オブジェクトをセットアップします。次に、Transact
がラムダのクローンを呼び出します。これは、ラムダとまったく同様であるものの、トランザクション インスツルメンテーションを含んでおり、AutoRTFM ABI を遵守しています。Transact
を呼び出します。将来的には、Verse の実行に入る前に、Transact
を呼び出します。malloc/free
と関連する関数 (カスタム アロケータなど) です。WriteFile
などの関数を呼び出すと、デフォルトでエラーになります。これは望ましいデフォルトの動作です。WriteFile
は確実に取り消すことができないからです。後で書き込みを取り消そうと試みても、(自分のプロセス内または別のプロセス内の) 他のスレッドがその書き込みを認識して、それに基づいて回復不可能な別の処理を実行している可能性があるからです。このため、実践的なのは、既存の C++ プログラムを AutoRTFM でコンパイルし、Transact
を実行することです。これが正しく動作するか、トランザクションセーフでない関数を呼び出したというエラーが表示され、その関数と呼び出しサイトの検索方法に関するメッセージが表示されます。malloc
とその関連関数です。AutoRTFM は、malloc
の実装をトランザクション化せず、呼び出し元が malloc
への呼び出しを AutoRTFM API でラップすることを強制するというアプローチをとっています。これは、実際の malloc
実装をラップするファサード API がすでにある Unreal Engine などのプログラムでは特に簡単です。コードを確認して、どのような処理が行われているのかを理解することをお勧めします。AutoRTFM 以前の FMemory::Malloc
は次のようなコードでした。void*
FMemory::
Malloc
(SIZE_T
Count
, uint32
Alignment
)
{
return
GMalloc
->
Malloc
(Count, Alignment);
}
void*
FMemory::
Malloc
(SIZE_T
Count
, uint32
Alignment
)
{
void*
Ptr;
UE_AUTORTFM_OPEN
(
{
Ptr =
GMalloc
->
Malloc
(Count, Alignment);
});
AutoRTFM::
OnAbort
([
Ptr
]
{
Free
(Ptr);
});
return
AutoRTFM::
DidAllocate
(Ptr, Count);
}
UE_AUTORTFM_OPEN
を使用することで、囲まれたブロック (ラムダに変換される) がトランザクション インスツルメンテーションなしで実行する必要があることを指定します。これは、ランタイムとコンパイラが実装するのが簡単で、元の (クローンされていない) バージョンのラムダを呼び出すだけであることを意味するからです。つまり、GMalloc->Malloc
はトランザクションの内部で実行されるものの、トランザクション インスツルメンテーションは行われません。これを「オープンで実行する」といいます。(クローズドで実行するのとは対照的に、トランザクション化されたクローンを実行することを意味します)。つまり、この UE_AUTORTFM_OPEN
が戻った後、malloc 呼び出しは、ロールバックが登録されることなく、直ちに完了するまで実行されます。これにより、トランザクション セマンティクスが維持されます。malloc はすでにスレッドセーフであるため、このスレッドが malloc を実行したという事実は、malloc されたオブジェクトへのポインタがトランザクションからリークするまで、他のスレッドからは確認できないためです。ポインタは、コミットするまでトランザクション外にリークしません。これは、Ptr
が格納される場所が、新しく割り当てられた他のメモリか、AutoRTFM が書き込みをログに記録してトランザクション化するメモリのみであるからです。AutoRTFM::OnAbort
を使用してオブジェクトを解放するための中断時ハンドラを登録します。トランザクションがコミットすると、このハンドラは実行されないため、malloc されたオブジェクトが存続します。しかし、トランザクションが中断した場合は、オブジェクトを解放します。これにより、中断によるメモリ リークが発生しないことが保証されます。OnAbort
を実行しやすくなります。これは、すでに解放したオブジェクトの内容を、AutoRTFM が上書きして以前の状態に戻そうとする心配がなくなるからです。また、新しく割り当てられたメモリへの書き込みをログに記録しないことも、有用な最適化です。UE_AUTORTFM_OPEN
は、単にコード ブロックを実行します。AutoRTFM::OnAbort
は、何も実行しません。渡されたラムダは無視されます。AutoRTFM::DidAllocate
は何も実行しません。void
FMemory::
Free
(
void*
Original
)
{
UE_AUTORTFM_ONCOMMIT
(
{
GMalloc
->
Free
(Original);
});
}
UE_AUTORTFM_ONCOMMIT
関数の動作は次のとおりです。
Open/OnCommit/OnAbort
API を使用した malloc/GC インスツルメンテーションを組み合わせることで、アトミック、ロック、システム呼び出し、またはトランザクション化しないことを選択した Unreal Engine のコードベース部分 (現在のところ、レンダリング エンジンや物理エンジンなど) への呼び出しを使用しない限り、トランザクションで C++ コードを実行するために必要な要素がすべて揃います。このことが、フォートナイト全体で合計 94 個の Open
API の使用につながりました。これらを使用するには、アルゴリズムやデータ構造を変更するのではなく、適切な粒度で既存のコードを Open
および friend でラップする必要があります。<no_rollback>
エフェクトを削除し、
if
および for
からより多くのコードを呼び出せるようになりました。ほとんどの C++ コードがトランザクション化に対応できないため、このエフェクトは、多くの関数シグネチャを汚染しているだけです。AutoRTFM がこの問題を解決します。