MLIR의 소유권 기반 버퍼 할당 해제 패스와 함수 경계 ABI 규칙, bufferization.dealloc의 삽입·최적화·하향화 절차, 지원 인터페이스와 제한 사항, 예제, 단순화 및 하향화 패스를 설명합니다.
One-Shot Bufferize는 자신이 할당한 버퍼를 해제하지 않습니다. One-Shot Bufferize 실행 후 생성된 IR에는 여러 개의 memref.alloc 연산이 있을 수 있지만 memref.dealloc 연산은 없습니다. 버퍼 할당 해제는 -ownership-based-buffer-deallocation 패스에 위임됩니다.
높은 수준에서, 버퍼는 기본 블록이 “소유”합니다. 소유권은 i1 SSA 값으로 구체화되며 “해제 책임”이라고 생각할 수 있습니다. 이는 C++의 std::unique_ptr와 개념적으로 유사합니다.
소유권 기반 버퍼 할당 해제 패스와 함께 실행해야 하는 몇 가지 추가 전처리·후처리 패스가 있습니다. 권장 컴파일 파이프라인은 다음과 같습니다:
one-shot-bufferize
| 이 시점까지 모든 버퍼라이제이션을 끝내는 것을 권장합니다.
| <- 이후에 삽입되는 어떤 할당도 수동으로 처리해야 합니다.
V
expand-realloc
V
ownership-based-buffer-deallocation
V
canonicalize <- 주로 scf.if 단순화를 위해
V
buffer-deallocation-simplification
V <- 이 지점부터는 tensor 값이 허용되지 않습니다.
lower-deallocations
V
CSE
V
canonicalize
전체 할당 해제 파이프라인(-one-shot-bufferize 제외)은 -buffer-deallocation-pipeline으로 제공됩니다.
소유권 기반 버퍼 할당 해제 패스는 FunctionOpInterface를 구현하는 연산을 호출 그래프를 분석하지 않고 하나씩 처리합니다. 이는 MemRef가 한 함수에서 다른 함수로 전달될 때 어떻게 다루어져야 하는지에 대한 일부 규칙이 필요함을 의미합니다. 패스의 나머지 부분은 각 기본 블록의 끝에 적절한 피연산자와 함께 삽입되는 bufferization.dealloc 연산에 크게 의존하며, 이는 버퍼 할당 해제 단순화 패스(--buffer-deallocation-simplification)와 일반 canonicalizer(--canonicalize)로 최적화되어야 합니다. -ownership-based-buffer-deallocation의 결과를 사전 최적화 없이 곧바로 --convert-bufferization-to-memref로 낮추는 것은 권장되지 않습니다. 매우 비효율적인 코드가 생성되기 때문입니다(bufferization.dealloc의 런타임 비용은 O(|memrefs|^2+|memref|*|retained|)).
버퍼 할당 해제 패스는 FunctionOpInterface를 구현하는 연산 단위에서 동작합니다. 이러한 연산은 MemRef를 인수로 받을 수도, 반환할 수도 있습니다. 모든 함수(외부 함수 포함) 간의 호환성을 보장하려면 다음 규칙을 강제해야 합니다:
외부 함수(예: C로 작성된 라이브러리 함수)의 경우, 외부에서 제공된 구현이 이러한 규칙을 준수해야 하며, 버퍼 할당 해제 패스는 이를 가정합니다. 할당 해제 패스가 적용되고 구현에 접근 가능한 함수는 ABI가 준수되도록 패스에 의해 수정됩니다(즉, 필요 시 버퍼 복사가 삽입됩니다).
bufferization.dealloc 연산 삽입¶bufferization.dealloc과 소유권 지시자는 소유권 기반 버퍼 할당 해제 패스의 주요 추상화입니다. bufferization.dealloc은 해당 소유권 지시자가 설정되어 있고 보존 목록에 에일리어싱하는 버퍼가 없는 경우 전달된 모든 버퍼를 해제합니다.
bufferization.dealloc 연산은 각 기본 블록의 끝(terminator 바로 앞)에 무조건 삽입됩니다. 패스의 대부분은 이 연산의 올바른 피연산자를 찾는 일입니다. 채워야 할 가변 길이 피연산자 목록이 세 가지 있으며, 첫 번째는 해제가 필요할 수 있는 모든 MemRef 값을, 두 번째는 해당 소유권 값(i1 타입)을, 세 번째는 이후 시점에 여전히 필요하므로 해제하지 않아야 하는 MemRef 값을 포함합니다(예: yield되거나 반환되는 버퍼).
bufferization.dealloc은 다양한 종류의 에일리어싱 동작을 처리할 수 있게 해줍니다. 정적으로 충분한 정보를 수집할 수 없을 때는 런타임 에일리어싱 검사로 낮춰집니다. 정적으로 충분한 에일리어싱 정보가 있으면 일부 피연산자 혹은 연산 전체가 접힐(fold) 수 있습니다.
소유권(Ownerships)
이를 위해, memref의 소유권 지시자 개념을 사용합니다. 이는 어떤 memref 타입 SSA 값에 대해서든 i1 값으로 구체화되며, 해당 값이 구체화된 기본 블록이 이 MemRef의 소유권을 가지는지 나타냅니다. 이상적으로는 이 값은 상수 true 또는 false이지만, 비상수 SSA 값일 수도 있습니다. 이러한 소유권 값을 즉시 구체화하지 않고 추적하기 위해(bufferization.clone 연산 삽입이나, 실제로 필요하지 않은 위치에서의 런타임 에일리어싱 검사 삽입을 피하기 위해) Ownership 클래스를 사용합니다. 이 클래스는 부분 순서 위의 격자(lattice)를 이루는 세 가지 상태로 소유권을 표현합니다:
forall X in SSA values. uninitialized < unique(X) < unknown
forall X, Y in SSA values.
unique(X) == unique(Y) iff X and Y always evaluate to the same value
unique(X) != unique(Y) otherwise
직관적으로 상태의 의미는 다음과 같습니다:
arith.select의 결과는 조건값에 따라 then/else 케이스의 소유권 중 하나를 갖습니다. 소유권 값에 대해 또 다른 arith.select를 삽입하면 병합을 수행하여 결과에 ‘Unique’ 소유권을 제공할 수 있습니다). 그러나 일반적인 경우에는 이 ‘Unknown’ 상태를 부여해야 합니다.위 부분 순서에 의해, 패스는 두 소유권을 다음과 같이 결합합니다:
| 소유권 1 | 소유권 2 | 결합된 소유권 |
|---|---|---|
| uninitialized | uninitialized | uninitialized |
| unique(X) | uninitialized | unique(X) |
| unique(X) | unique(X) | unique(X) |
| unique(X) | unique(Y) | unknown |
| unknown | unique | unknown |
| unknown | uninitialized | unknown |
| + 대칭 경우 |
해제가 필요할 수 있는 MemRef 목록 수집
주어진 블록에 대해, 해당 블록 끝에서 해제가 필요할 수 있는 MemRef 목록은 블록이 소유권을 인수할 가능성이 있는 모든 값을 추적함으로써 계산됩니다. 여기에는 기본 블록 인수로 제공된 MemRef, memref.alloc 및 func.call과 같은 연산에 대한 인터페이스 핸들러, 그리고 여러 기본 블록을 가진 리전에서의 라이브니스 정보가 포함됩니다. 보다 구체적으로는 현재 기본 블록 B의 라이브니스 분석 ‘in’ 집합에 있는 MemRef에, MemRef 블록 인수 및 B 자체에서 할당된 MemRef 집합(인터페이스 핸들러가 결정)을 추가하고, 그 다음 B에서 해제된 MemRef 집합(역시 인터페이스 핸들러가 결정)을 빼서 계산합니다.
선행 블록의 ‘out’ 집합과 라이브니스 ‘in’ 집합의 교집합을 취할 필요가 없음을 유의하십시오. ‘in’ 집합에 있는 값은 모든 직접 선행자를 지배(dominate)하는 조상 블록에서 정의되어야 하므로, 이 블록의 ‘in’ 집합은 각 선행자의 ‘out’ 집합의 부분집합입니다.
memrefs = filter((liveIn(block) U
allocated(block) U arguments(block)) \ deallocated(block), isMemRef)
bufferization.dealloc의 두 번째 가변 피연산자 목록(조건 목록)은 위에서 설명한 방식으로 수집된 각 MemRef에 대해 저장된 소유권 값을 질의하여 계산됩니다. 블록을 처리하는 동안 인터페이스 핸들러가 소유권 상태를 갱신합니다.
보존해야 할 MemRef 목록 수집
기본 블록 B가 주어졌을 때, 보존해야 할 MemRef 목록은 후속 블록 S마다 다를 수 있습니다. 두 기본 블록 B와 S 및 목적지 블록 S로 블록 인수를 통해 전달되는 값들에 대해, terminator의 successor 피연산자 목록에 있는 MemRef와 B의 라이브니스 분석 ‘out’ 집합을 목적지 블록 S의 ‘in’ 집합과 교집합하여, B에서 보존해야 할 MemRef 목록을 계산합니다.
이 보존 값 목록은 컴파일 타임에 에일리어싱 정보가 없더라도 use-after-free 상황이 발생하지 않도록 보장합니다.
toRetain = filter(successorOperands + (liveOut(fromBlock) insersect
liveIn(toBlock)), isMemRef)
이 패스는 라이브니스 분석과 몇 가지 인터페이스를 사용합니다:
FunctionOpInterfaceCallOpInterfaceMemoryEffectOpInterfaceRegionBranchOpInterfaceRegionBranchTerminatorOpInterface인터페이스가 제공하는 정보가 불충분하기 때문에, 현재는 cf.cond_br 연산에 대한 특수 처리를 수행하고 RegionBranchOpInterface를 구현하는 연산에 대해 몇 가지 가정을 합니다. 향후 인터페이스가 개선되면 이러한 의존성을 제거할 수 있습니다.
버퍼 할당 해제 패스는 입력 IR에 대해 몇 가지 요구 사항과 제한 사항을 가집니다. 이는 패스 시작 시 확인되며, 해당되지 않으면 오류가 발생합니다:
RegionBranchOpInterface를 구현하지 않는 경우, 패스는 중첩 리전의 의미론을 알 수 없으므로 오류를 발생시킵니다(기본 가정을 하지는 않습니다).RegionBranchTerminatorOpInterface 또는 BranchOpInterface 중 하나만 구현해야 합니다(둘 다 구현하면 안 됩니다). 둘 이상의 successor를 가진 terminator는 지원하지 않습니다(cf.cond_br는 예외). 이는 근본적 제한은 아니지만, 현재로서는 더 복잡한 구현을 정당화할 사용 사례가 없습니다.다음 예제는 몇 가지 흥미로운 경우를 포함합니다:
arith.select의 결과는 초기에는 소유권이 ‘Unknown’으로 할당되지만, 일단 bufferization.dealloc 연산이 삽입되면(나중 기본 블록에서 사용되므로) ‘보존’ 목록에 들어갑니다. 따라서 dealloc 연산의 해당 결과를 사용하여 ‘Unknown’ 소유권을 ‘Unique’ 소유권으로 대체할 수 있습니다.cf.cond_br 연산은 둘 이상의 successor를 가지므로 두 개의 bufferization.dealloc 연산(각 successor마다 하나)을 삽입해야 합니다. 두 연산은 같은 블록에 대한 해제를 수행하므로 해제할 MemRef 목록은 동일하지만, 어떤 MemRef는 한 분기에서는 여전히 라이브이고 다른 분기에서는 그렇지 않을 수 있습니다(따라서 현재 블록의 _라이브-아웃_과 대상 블록의 _라이브-인_에 대한 교집합이 수행됩니다). 또한 cf.cond_br는 successor별로 별도의 전달 피연산자를 지원합니다. 동일한 MemRef를 해제하는 두 개의 bufferization.dealloc 연산이 있기 때문에 어떤 MemRef도 두 번 해제되지 않도록, 조건 피연산자를 분기 조건을 고려하여 조정합니다. 이러한 terminator 연산에 대한 일반 하향화를 구현할 수도 있지만, 특수화된 구현은 이 특정 연산의 의미론을 모두 고려할 수 있으므로 보다 효율적인 하향화를 생성할 수 있습니다.func.func @example(%memref: memref<?xi8>, %select_cond: i1, %br_cond: i1) {
%alloc = memref.alloc() : memref<?xi8>
%alloca = memref.alloca() : memref<?xi8>
%select = arith.select %select_cond, %alloc, %alloca : memref<?xi8>
cf.cond_br %br_cond, ^bb1(%alloc : memref<?xi8>), ^bb1(%memref : memref<?xi8>)
^bb1(%bbarg: memref<?xi8>):
test.copy(%bbarg, %select) : (memref<?xi8>, memref<?xi8>)
return
}
--ownership-based-buffer-deallocation 실행 후는 다음과 같습니다:
// 함수 경계 ABI: `%memref`의 소유권은 획득되지 않습니다.
func.func @example(%memref: memref<?xi8>, %select_cond: i1, %br_cond: i1) {
%false = arith.constant false
%true = arith.constant true
// `memref.alloc` 연산이 정의한 MemRef의 소유권은 항상 'true'로
// 할당됩니다.
%alloc = memref.alloc() : memref<?xi8>
// `memref.alloca` 연산이 정의한 MemRef의 소유권은 항상 'false'로
// 할당됩니다.
%alloca = memref.alloca() : memref<?xi8>
// %select의 소유권은 %alloc과 %alloca의 소유권(즉 %true와 %false)의
// 조인입니다. 패스는 `arith.select`의 의미론을 알지 못하므로(커스텀
// 핸들러가 구현되지 않는 한) 소유권 조인은 'Unknown'이 됩니다. %select의
// 구체화된 소유권 지시자가 필요하다면, %true가 소유권으로 할당되는
// 복제본을 만들거나 %select가 보존 목록에 포함되는 `bufferization.dealloc`
// 결과를 사용해야 합니다.
%select = arith.select %select_cond, %alloc, %alloca : memref<?xi8>
// 임의의 memref를 `memref.dealloc`에 전달하는 것은 허용되지 않으므로,
// base memref를 얻기 위해 `memref.extract_strided_metadata`를 사용합니다.
// 이 속성은 이미 `bufferization.dealloc`에 대해 강제됩니다.
%base_buffer_memref, ... = memref.extract_strided_metadata %memref
: memref<?xi8> -> memref<i8>, index, index, index
%base_buffer_alloc, ... = memref.extract_strided_metadata %alloc
: memref<?xi8> -> memref<i8>, index, index, index
%base_buffer_alloca, ... = memref.extract_strided_metadata %alloca
: memref<?xi8> -> memref<i8>, index, index, index
// 해제 조건은 분기 조건을 반영하도록 조정해야 합니다. 이 예시에서는
// 단순히 한 번의 부정만 필요하지만, 여러 개의 arith.andi가 필요할 수도
// 있습니다.
%not_br_cond = arith.xori %true, %br_cond : i1
// 이 기본 블록에는 successor마다 하나씩 두 개의 dealloc 연산이
// 삽입됩니다. 둘 다 해제할 MemRef 목록은 동일하며, 조건은 분기 조건의
// 결합만 다릅니다.
// 단, 보존 목록은 다릅니다. 두 경우 모두 같은 블록이므로 두 successor에서
// %select가 사용되어 보존 목록에 포함되지만, 블록 인수를 통해 전달되는
// 값은 다릅니다(%memref vs. %alloc).
%10:2 = bufferization.dealloc
(%base_buffer_memref, %base_buffer_alloc, %base_buffer_alloca
: memref<i8>, memref<i8>, memref<i8>)
if (%false, %br_cond, %false)
retain (%alloc, %select : memref<?xi8>, memref<?xi8>)
%11:2 = bufferization.dealloc
(%base_buffer_memref, %base_buffer_alloc, %base_buffer_alloca
: memref<i8>, memref<i8>, memref<i8>)
if (%false, %not_br_cond, %false)
retain (%memref, %select : memref<?xi8>, memref<?xi8>)
// %select가 ^bb1에서 블록 인수로 전달되지 않고 사용되므로, 여기서 dealloc
// 연산이 반환한 소유권 값을 병합하여 %select의 소유권 값을 갱신해야
// 합니다.
%new_ownership = arith.select %br_cond, %10#1, %11#1 : i1
// terminator는 각 MemRef 값과 함께 소유권 지시자 값을 전달하도록
// 수정됩니다.
cf.cond_br %br_cond, ^bb1(%alloc, %10#0 : memref<?xi8>, i1),
^bb1(%memref, %11#0 : memref<?xi8>, i1)
// 엔트리 이외의 모든 기본 블록은 인자 목록의 각 MemRef 값에 대해 추가로
// 하나의 i1 인자를 갖도록 수정됩니다.
^bb1(%13: memref<?xi8>, %14: i1): // 2 preds: ^bb0, ^bb0
test.copy(%13, %select) : (memref<?xi8>, memref<?xi8>)
%base_buffer_13, ... = memref.extract_strided_metadata %13
: memref<?xi8> -> memref<i8>, index, index, index
%base_buffer_select, ... = memref.extract_strided_metadata %select
: memref<?xi8> -> memref<i8>, index, index, index
// 여기서는 successor가 없고, return에 피연산자가 없기 때문에 보존 목록이
// 없습니다.
bufferization.dealloc (%base_buffer_13, %base_buffer_select
: memref<i8>, memref<i8>)
if (%14, %new_ownership)
return
}
bufferization.dealloc 연산의 의미론은 탐욕적 패턴 리라이터를 이용해 패턴으로 편리하게 나눌 수 있는 많은 최적화 기회를 제공합니다. 일부 패턴은 두 MemRef 값이 동일한 버퍼 할당에서 왔는지 “반드시/가능/절대 아님”을 판별할 수 있는 추가 분석에 접근해야 합니다. 이러한 패턴은 버퍼 할당 해제 단순화 패스에 모아두고, 추가 분석이 필요 없는 패턴은 일반 canonicalizer 패스의 일부로 등록됩니다. 이 패스는 --ownership-based-buffer-deallocation 후 --canonicalize에 이어 실행하는 것이 가장 좋습니다.
이 패스는 다음과 같은 단순화를 위한 패턴을 적용합니다:
bufferization.dealloc 연산으로 분리하여, 해당 연산의 ‘memref’ 목록에 그 값만 포함되도록 합니다. 이는 최소 한 번의 런타임 에일리어싱 검사를 피하고, 이 새로운 bufferization.dealloc에 대해 더 효율적인 하향화를 사용할 수 있게 합니다.-lower-deallocations 패스는 모든 bufferization.dealloc 연산을 memref.dealloc 연산으로 변환하며, 해제를 조건부로 만들고 두 MemRef 값이 런타임에 동일한 할당에서 왔는지를 확인하기 위해 scf, func, arith 다이얼렉트의 연산을 삽입할 수도 있습니다(이는 buffer-deallocation-simplification 패스가 정적으로 판별하지 못했을 때 필요합니다).
동일한 bufferization.dealloc의 하향화는 -convert-bufferization-to-memref 변환 패스의 일부이기도 하며, 이 패스는 버퍼라이제이션 다이얼렉트의 다른 모든 연산도 낮춥니다.
이 하향화 패스에서는 전체적으로 더 효율적인 하향화를 제공하기 위해 여러 경우를 구분합니다. 일반적인 경우에는(해당 dealloc 연산의 피연산자 수에 대해) 코드 크기의 이차적 폭증을 피하기 위해 라이브러리 함수를 생성합니다. 특수화된 하향화는 보조 index MemRef를 할당해야 하는 이 라이브러리 함수를 피하는 것을 목표로 합니다.
코드 크기 폭증을 피하기 위해 라이브러리 함수를 생성합니다. 높은 수준에서, 모든 피연산자의 base-memref를 index 값으로 추출하여 전용으로 할당한 MemRef에 저장하고, 이를 라이브러리 함수에 전달하여 동일한 원래 할당에서 왔는지 판별합니다. 이 정보는 이중 해제(double-free) 상황을 피하고, retained 목록의 MemRef 값을 올바르게 보존하는 데 필요합니다.
Dealloc 연산 하향화
이 하향화는 dealloc 연산이 제공하는 모든 기능을 지원합니다. 각 memref의 base 포인터를 계산하여(index로) 새로운 memref 헬퍼 구조에 저장하고, buildDeallocationLibraryFunction에서 생성된 헬퍼 함수에 전달합니다. 결과는 불리언 리스트(각각 MemRef로 표현됨) 두 개에 저장되어 인자로 전달됩니다. 첫 번째 리스트는 해당 조건의 메모리를 해제해야 하는지 여부를 저장하고, 두 번째 리스트는 보존된 값의 소유권을 저장하여 bufferization.dealloc 연산의 결과 값을 대체하는 데 사용할 수 있습니다.
예:
%0:2 = bufferization.dealloc (%m0, %m1 : memref<2xf32>, memref<5xf32>)
if (%cond0, %cond1)
retain (%r0, %r1 : memref<1xf32>, memref<2xf32>)
하향화(단순화된 형태):
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%dealloc_base_pointer_list = memref.alloc() : memref<2xindex>
%cond_list = memref.alloc() : memref<2xi1>
%retain_base_pointer_list = memref.alloc() : memref<2xindex>
%m0_base_pointer = memref.extract_aligned_pointer_as_index %m0
memref.store %m0_base_pointer, %dealloc_base_pointer_list[%c0]
%m1_base_pointer = memref.extract_aligned_pointer_as_index %m1
memref.store %m1_base_pointer, %dealloc_base_pointer_list[%c1]
memref.store %cond0, %cond_list[%c0]
memref.store %cond1, %cond_list[%c1]
%r0_base_pointer = memref.extract_aligned_pointer_as_index %r0
memref.store %r0_base_pointer, %retain_base_pointer_list[%c0]
%r1_base_pointer = memref.extract_aligned_pointer_as_index %r1
memref.store %r1_base_pointer, %retain_base_pointer_list[%c1]
%dyn_dealloc_base_pointer_list = memref.cast %dealloc_base_pointer_list :
memref<2xindex> to memref<?xindex>
%dyn_cond_list = memref.cast %cond_list : memref<2xi1> to memref<?xi1>
%dyn_retain_base_pointer_list = memref.cast %retain_base_pointer_list :
memref<2xindex> to memref<?xindex>
%dealloc_cond_out = memref.alloc() : memref<2xi1>
%ownership_out = memref.alloc() : memref<2xi1>
%dyn_dealloc_cond_out = memref.cast %dealloc_cond_out :
memref<2xi1> to memref<?xi1>
%dyn_ownership_out = memref.cast %ownership_out :
memref<2xi1> to memref<?xi1>
call @dealloc_helper(%dyn_dealloc_base_pointer_list,
%dyn_retain_base_pointer_list,
%dyn_cond_list,
%dyn_dealloc_cond_out,
%dyn_ownership_out) : (...)
%m0_dealloc_cond = memref.load %dyn_dealloc_cond_out[%c0] : memref<2xi1>
scf.if %m0_dealloc_cond {
memref.dealloc %m0 : memref<2xf32>
}
%m1_dealloc_cond = memref.load %dyn_dealloc_cond_out[%c1] : memref<2xi1>
scf.if %m1_dealloc_cond {
memref.dealloc %m1 : memref<5xf32>
}
%r0_ownership = memref.load %dyn_ownership_out[%c0] : memref<2xi1>
%r1_ownership = memref.load %dyn_ownership_out[%c1] : memref<2xi1>
memref.dealloc %dealloc_base_pointer_list : memref<2xindex>
memref.dealloc %retain_base_pointer_list : memref<2xindex>
memref.dealloc %cond_list : memref<2xi1>
memref.dealloc %dealloc_cond_out : memref<2xi1>
memref.dealloc %ownership_out : memref<2xi1>
// %0#0을 %r0_ownership으로 대체
// %0#1을 %r1_ownership으로 대체
라이브러리 함수
컴파일 단위마다 하나의 라이브러리 함수를 생성하여, bufferization dealloc 지점에서 두 MemRef가 동일한 할당에서 왔는지와 새로운 소유권을 판정하도록 합니다.
생성된 함수는 두 개의 index MemRef와 세 개의 불리언 MemRef를 인자로 받습니다:
extract_aligned_pointer_as_index를 적용한 결과를 포함해야 합니다.extract_aligned_pointer_as_index를 적용한 결과를 포함해야 합니다.이 헬퍼 함수는 각 bufferization.dealloc 연산마다 한 번 호출되어, 해제 필요성과 보존된 값의 새로운 소유권 지시자를 결정하지만, 실제 해제는 수행하지 않습니다.
생성 코드:
func.func @dealloc_helper(
%dyn_dealloc_base_pointer_list: memref<?xindex>,
%dyn_retain_base_pointer_list: memref<?xindex>,
%dyn_cond_list: memref<?xi1>,
%dyn_dealloc_cond_out: memref<?xi1>,
%dyn_ownership_out: memref<?xi1>) {
%c0 = arith.constant 0 : index
%c1 = arith.constant 1 : index
%true = arith.constant true
%false = arith.constant false
%num_dealloc_memrefs = memref.dim %dyn_dealloc_base_pointer_list, %c0
%num_retain_memrefs = memref.dim %dyn_retain_base_pointer_list, %c0
// 결과 버퍼 0 초기화
scf.for %i = %c0 to %num_retain_memrefs step %c1 {
memref.store %false, %dyn_ownership_out[%i] : memref<?xi1>
}
scf.for %i = %c0 to %num_dealloc_memrefs step %c1 {
%dealloc_bp = memref.load %dyn_dealloc_base_pointer_list[%i]
%cond = memref.load %dyn_cond_list[%i]
// 보존된 memref와의 에일리어싱 검사
%does_not_alias_retained = scf.for %j = %c0 to %num_retain_memrefs
step %c1 iter_args(%does_not_alias_aggregated = %true) -> (i1) {
%retain_bp = memref.load %dyn_retain_base_pointer_list[%j]
%does_alias = arith.cmpi eq, %retain_bp, %dealloc_bp : index
scf.if %does_alias {
%curr_ownership = memref.load %dyn_ownership_out[%j]
%updated_ownership = arith.ori %curr_ownership, %cond : i1
memref.store %updated_ownership, %dyn_ownership_out[%j]
}
%does_not_alias = arith.cmpi ne, %retain_bp, %dealloc_bp : index
%updated_aggregate = arith.andi %does_not_alias_aggregated,
%does_not_alias : i1
scf.yield %updated_aggregate : i1
}
// 현재 것 이전의 dealloc memref와의 에일리어싱 검사, 즉
// `fix i, forall j < i: check_aliasing(%dyn_dealloc_base_pointer[j],
// %dyn_dealloc_base_pointer[i])`
%does_not_alias_any = scf.for %j = %c0 to %i step %c1
iter_args(%does_not_alias_agg = %does_not_alias_retained) -> (i1) {
%prev_dealloc_bp = memref.load %dyn_dealloc_base_pointer_list[%j]
%does_not_alias = arith.cmpi ne, %prev_dealloc_bp, %dealloc_bp
%updated_alias_agg = arith.andi %does_not_alias_agg, %does_not_alias
scf.yield %updated_alias_agg : i1
}
%dealloc_cond = arith.andi %does_not_alias_any, %cond : i1
memref.store %dealloc_cond, %dyn_dealloc_cond_out[%i] : memref<?xi1>
}
return
}
현재는 공통 사례에 대해 두 가지 특수 하향화를 제공하여 라이브러리 함수, 불필요한 메모리 로드/스토어, 함수 호출을 피합니다:
단일 memref, 보존 없음
보존 값이 없고 단일 MemRef만 있는 단순한 경우를 낮춥니다. 이상적으로는, 정적 분석이 충분한 정보를 제공하여 buffer-deallocation-simplification 패스가 이 패스를 실행하기 전에 가능한 한 많이 dealloc 연산을 이 단순한 형태로 분할할 수 있게 하기를 바랍니다.
예:
bufferization.dealloc (%arg0 : memref<2xf32>) if (%arg1)
하향화 결과:
scf.if %arg1 {
memref.dealloc %arg0 : memref<2xf32>
}
대부분의 경우, 분기 조건은 상수 ’true’ 또는 ‘false’이므로 canonicalizer 패스에 의해 완전히 제거될 수 있습니다.
단일 memref, 임의 개수의 보존
정확히 하나의 MemRef와 임의 개수의 보존 값을 가진 dealloc 연산에 대한 특수 하향화입니다. 생성되는 코드의 크기는 보존 값의 수에 선형입니다.
예:
%0:2 = bufferization.dealloc (%m : memref<2xf32>) if (%cond)
retain (%r0, %r1 : memref<1xf32>, memref<2xf32>)
return %0#0, %0#1 : i1, i1
하향화:
%m_base_pointer = memref.extract_aligned_pointer_as_index %m
%r0_base_pointer = memref.extract_aligned_pointer_as_index %r0
%r0_does_not_alias = arith.cmpi ne, %m_base_pointer, %r0_base_pointer
%r1_base_pointer = memref.extract_aligned_pointer_as_index %r1
%r1_does_not_alias = arith.cmpi ne, %m_base_pointer, %r1_base_pointer
%not_retained = arith.andi %r0_does_not_alias, %r1_does_not_alias : i1
%should_dealloc = arith.andi %not_retained, %cond : i1
scf.if %should_dealloc {
memref.dealloc %m : memref<2xf32>
}
%true = arith.constant true
%r0_does_alias = arith.xori %r0_does_not_alias, %true : i1
%r0_ownership = arith.andi %r0_does_alias, %cond : i1
%r1_does_alias = arith.xori %r1_does_not_alias, %true : i1
%r1_ownership = arith.andi %r1_does_alias, %cond : i1
return %r0_ownership, %r1_ownership : i1, i1