var
X
=
0
if
:
set
X
=
42
# 이 줄은 다음 줄 때문에 롤백됩니다
# 다음 줄
X
>
100
# 이것은 실패하여 롤백을 유발합니다
X
=
0
# X는 롤백으로 인해 0으로 돌아갑니다
set
명령뿐만 아니라 모든 이펙트에 적용됩니다. 예를 들면, 변경 가능한 어떤 변수를 수정하여(즉, 포인터에 저장)하는 C++ 함수를 호출하고 다른 C++ 함수를 사용하여 해당 변경 가능 함수를 읽어들일 수 있습니다.SetSomething
(
0
)
# C++ 구현, C++ 변수를 설정합니다
# 변수
if
:
SetSomething
(
42
)
# 이 줄은 다음 줄 때문에 롤백됩니다
# 다음 줄
GetSomething
()
>
100
# 이것은 실패하여 롤백을 유발합니다
GetSomething
()
=
0
# 롤백으로 인해 0으로 돌아갑니다
if
문의 조건과 같은 Verse 실패 컨텍스트는 트랙잭션에 중첩되어, 조건이 성공하면 커밋되고 아니면 중단됩니다. 조건부 표현식은 이펙트를 가질 수 있습니다. 조건이 실패하면 마치 이펙트가 전혀 발생하지 않은 것과 같습니다.<no_rollback>
이펙트로 표시되며, 이 문제가 해결되는 즉시 폐기될 것입니다.<no_rollback>
이펙트) 오류가 발생하기 쉬워, 삭제되는 항목에 실행 취소 핸들러가 등록되는 등 디버깅이 어려운 크래시를 유발합니다.void
Foo
()
{
TArray
<int> Array
=
{
1
,
2
,
3
};
AutoRTFM::
Transact
(
[
&
] ()
{
Array
.
Push
(
42
);
//
Array is {1, 2, 3, 42} here.
AutoRTFM::
Abort
();
});
//
중단으로 인해 배열이 {1, 2, 3}이 됩니다.
}
TArray
가 변경되지 않고 작동합니다. 심지어 TArray
가 Push
내부의 백업 저장공간에 다시 할당해야 하더라도 작동합니다. TArray
가 특별한 것이 아닙니다. AutoRTFM은 std::vector
/std::map
및 언리얼 엔진과 C++ 표준 라이브러리의 수많은 기타 데이터 구조와 함수에 대해서도 이 작업을 수행할 수 있습니다. 이렇게 작동하도록 만들려면 AutoRTFM을 로우 레벨 메모리 할당자 및 가비지 콜렉터(Garbage Collector, GC)와 통합하기만 하면 됩니다. 이 섹션에서 보여드리겠지만 이러한 변경사항도 대단히 사소한 것입니다. 단순한 코드 추가이며, 그것도 단 몇 줄밖에 되지 않습니다. 언리얼 엔진의 많은 malloc, 언리얼 엔진의 GC, 시스템 malloc/new 모두에 해당 코드를 추가했습니다. 뿐만 아니라 AutoRTFM은 트랜잭션 외부(Transact
호출의 동적 범위 외부)에서 실행되는 코드 경로에 대해 이 모든 것을 제로 퍼포먼스 리그레션으로 달성합니다. 중요한 것은 TArray
외 다수의 코드 경로가 트랜잭션 내에서 사용되더라도 외부 트랜잭션을 사용하는 항목들이 비용을 초래하지 않는다는 것입니다. 코드가 트랜잭션 내에서 실행되는 경우 오버헤드가 높지만(내부 벤치마크에서 4배까지 확인된 바 있음), 포트나이트 서버의 틱 속도를 저해할 만큼 높지는 않습니다.AutoRTFMPass
의 역할은 모든 코드를 검사하여 Verse의 트랜잭션 시맨틱을 따르게 하는 것입니다. 그러나 포트나이트의 동일한 코드가 어떨때는 스택에 Verse 코드가 없고 Verse 시맨틱이 필요하지 않은 위치에서 사용될 수도 있고 어떨때는 스택에 Verse가 있고 롤백이 가능한 위치에서 사용될 수 있습니다.bCurrentThreadIsInTransaction
비트가 프로그램 카운터에 인코딩됩니다. 컴파일러의 관점에서 보면 간단합니다. 모든 코드를 복제하고 복제된 함수에 맹글링된 이름을 부여한 다음 클론만 검사하는 것입니다. 원본 코드는 검사되지 않은 상태이며 로깅이나 기타 작업을 하지 않기 때문에 코드 실행에 따른 비용은 0입니다. 복제된 코드는 검사를 통해 트랜잭션에 있음을 분명히 알게 됩니다. 따라서 대체 호출 규칙 등 VM 같은 트릭을 더욱 원활하게 활용할 수 있습니다.AutoRTFMPass
는 LLVM 파이프라인에서 새니타이저와 동일한 시점, 즉 모든 IR 최적화가 이뤄지고 코드가 백엔드로 전송되기 직전에 실행됩니다. 이는 중요한 의미를 담고 있습니다. 최적화를 거치고 살아 남은 불러오기 및 저장만 검사를 받는 대상이 됩니다. 따라서 C++에서 비탈출(non-escaping) 로컬 변수에 할당하는 것은 추상적인 의미로 '저장'이지만(Clang은 해당 프로그램에 대해 최적화되지 않은 LLVM IR 생성 시 이 방식으로 표현), 이 저장은 AutoRTFMPass
기능이 동작하면 완전히 사라집니다. 이는 높은 확률로 SSA 데이터플로에 의해 대체되므로 AutoRTFM가 수행될 필요가 없습니다. 또한 기존 LLVM 통계 분석을 기반으로 몇 가지 간단한 최적화를 구현하여, 저장의 검사가 불필요함을 입증 가능한 경우 해당 단계를 건너뛰게 만들었습니다.AutoRTFM::Transact
입니다. 이 함수를 람다로 호출하면 런타임이 로그 데이터 구조 및 기타 트랜잭션 상태를 포함하는 트랜잭션 오브젝트를 구성합니다. 그런 다음 Transact
가 람다의 클론을 호출합니다. 이는 람다와 완전히 동일하지만 트랜잭션 검사를 거치며 AutoRTFM ABI를 따릅니다.Transact
를 if 조건문 등 실패 컨텍스트에 들어가는 Verse에 응답하여 호출합니다. 향후에는 Verse 실행에 들어가기 전에 Transact
를 호출할 것입니다.malloc/free
및 관련 함수(커스텀 할당자 등)입니다.WriteFile
같은 것을 호출하면 기본적으로 오류가 발생합니다. 이는 바람직한 기본 동작입니다. WriteFile
은 안정적으로 실행 취소될 수 없기 때문입니다. 설령 나중에 쓰기를 실행 취소한다고 해도 현재 프로세스나 다른 프로세스의 어떤 스레드에서 쓰기 완료된 상태에 복구 불가능한 무언가를 수행할 수도 있습니다. 이로 인해 기존 C++ 프로그램을 AutoRTFM으로 컴파일해서 Transact
를 시도하는 것이 가능해집니다. 올바르게 작동하거나, 아니면 트랜잭션에 안전하지 않은 함수를 호출했다는 오류와 함께 해당 함수와 CallSite를 찾는 방법에 대한 메시지가 표시됩니다.malloc
및 관련 함수입니다. AutoRTFM은 malloc
의 구현을 트랜잭션화하지 않는 접근법을 취하지만, 호출자에게 malloc
호출을 AutoRTFM API로 래핑하도록 강제합니다. 이는 실제 malloc
구현을 둘러싸는 파사드 API가 이미 있는 언리얼 엔진 같은 프로그램에서 특히 간단합니다. 코드를 직접 살펴보면 가장 이해하기 좋습니다. 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
실행이 쉬워집니다. 새로 할당된 메모리에 쓰기를 로깅하지 않으면 최적화에도 유용합니다.UE_AUTORTFM_OPEN
은 코드 블록만 실행합니다.AutoRTFM::OnAbort
는 동작하지 않습니다(no-op). 전달된 람다는 무시됩니다.AutoRTFM::DidAllocate
는 동작하지 않습니다(no-op).void
FMemory::
Free
(
void*
Original
)
{
UE_AUTORTFM_ONCOMMIT
(
{
GMalloc
->
Free
(Original);
});
}
UE_AUTORTFM_ONCOMMIT
함수는 다음과 같이 동작합니다.
Open/OnCommit/OnAbort
API를 사용한 malloc/GC 검사의 조합은 트랜잭션에서 C++ 코드를 실행하는 데 필요한 모든 것을 제공합니다. 이렇게 하여 포트나이트 전체에 총 94번의 Open
API 사용이 있었습니다. 이러한 사용에는 알고리즘이나 데이터 구조를 바꾸지 않고 Open
및 관련 API를 활용하여 기존 코드를 적당히 감싸는 것을 포함합니다.<no_rollback>
이펙트를 제거하면
if
및 for
구문으로부터 더 많은 코드를 호출할 수 있습니다. 이 이펙트가 발생하여 많은 함수 시그너처를 오염시키는 이유는 대부분의 C++ 코드가 트랜잭션화될 수 없기 때문입니다. AutoRTFM이 그 문제를 해결합니다.