MLIR에서 텐서 의미론을 memref 의미론으로 변환하는 버퍼화 전반을 소개한다. One-Shot Bufferize 패스의 개념과 목표, DPS(대상 전달) 스타일, 텐서/버퍼 경계 처리, 메모리 레이아웃, 확장 방법, 버퍼 복사 디버깅까지 다룬다.
MLIR에서 버퍼화(Bufferization)는 tensor 의미론을 갖는 op들을 memref 의미론을 갖는 op들로 변환하는 과정이다. 버퍼화와 관련된 MLIR 패스는 여러 개가 있으며, 보통 패스 파이프라인의 마지막 단계들 중 하나로 실행되어 memref op를 LLVM으로 내리기 직전에 수행된다. 이는 많은 변환이 텐서 영역에서 더 쉽거나 그곳에서만 지원되기 때문이다. 예를 들어, 먼저 텐서에 대해 타일/퓨즈/…한 다음 남은 IR을 버퍼화한다.
가장 중요한 버퍼화 패스는 One-Shot Bufferize이다. 이 패스는 tensor IR을 memref IR로 다시 쓴다. 추가로, IR을 전처리하여(예: IR이 보다 효율적으로 버퍼화될 수 있도록) 돕는 보조 패스들, 할당 끌어올리기 같은 버퍼 레벨 최적화, 그리고 버퍼 해제 op 삽입을 수행하여 결과 memref IR에 메모리 누수가 없도록 한다.
버퍼 해제 패스는 소유권 기반 버퍼 해제 파이프라인으로 대체되어 폐기되었다. 폐기된 패스에는 생성된 IR에서 메모리 누수를 일으킬 수 있는 제한 사항이 있다.
One-Shot Bufferize는 destination-passing style(DPS)을 따르는 IR을 대상으로, 공격적인 제자리(in-place) 버퍼화를 수행하도록 설계된 텐서 버퍼화 패스이다.
One-Shot Bufferize의 특징은 다음과 같다.
모놀리식(Monolithic): 단일 MLIR 패스가 모든 작업을 수행한다.
op 인터페이스를 통한 확장성: BufferizableOpInterface를 구현한 모든 op는 버퍼화할 수 있다.
함수 전체 단위 분석(whole-function at a time): 제자리 버퍼화 결정은 텐서의 SSA 정의-사용(use-def) 체인을 분석하여 이뤄진다. op 인터페이스 구현은 텐서 op를 memref op로 다시 쓰는 로직뿐 아니라, One-Shot Bufferize의 분석이 op의 버퍼화/메모리 의미론을 질의할 수 있도록 돕는 보조 메서드도 제공한다.
2단계(2-Phase): 내부적으로 버퍼화는 두 단계로 나뉜다. 먼저 전체 IR을 분석하여 버퍼화 결정을 내린다. 이후 IR을 버퍼화(재작성)한다. 분석 단계는 정확한 SSA 정의-사용 정보를 활용한다. 이 과정에서 점진적으로 별칭(alias) 및 동등성(equivalence) 집합을 구축하며, 사전 할당된 메모리로부터 사후 별칭 분석에 의존하지 않는다.
탐욕적(Greedy): 연산을 하나씩 분석하면서 해당 텐서 OpOperand에 대해 복사가 필요한지 즉석에서 결정한다. 휴리스틱으로 분석 순서를 정한다.
모듈식(Modular): 현재의 One-Shot 분석은 다른 분석으로 교체할 수 있다. 버퍼화는 AnalysisState, 특히 AnalysisState::isInPlace를 통해 분석 결과를 질의한다. 소수의 가상 함수를 구현한 AnalysisState 파생 클래스라면 사용자 정의 분석으로 사용할 수 있다. 심지어 어떤 분석도 없이(AlwaysCopyAnalysisState) One-Shot Bufferize를 실행하는 것도 가능하며, 이 경우 모든 버퍼 쓰기 전에 항상 복사를 수행한다.
참고: One-Shot Bufferize는 버퍼를 해제하지 않는다. 버퍼 해제는 소유권 기반 버퍼 해제 패스가 담당한다.
모든 버퍼화 기법의 상위 목표는 다음과 같다.
이는 가능한 경우 이미 할당된 버퍼를 재사용함을 의미하며, 이로 인해 버퍼화는 레지스터 할당과 유사한 알고리즘적 복잡성을 가진 문제가 된다.
구체적 사용 사례에 따라 추가적인 버퍼화 요구사항이 있을 수 있다. 버퍼의 내용 계산 비용이 비싸다면, 재계산(recomputation)과 한 번 계산 후 복사(compute once and copy) 사이에서 트레이드오프가 생길 수 있다. 반대로, 일부 아키텍처에서는 런타임에 새 버퍼를 할당하는 것 자체가 불가능할 수도 있다.
버퍼화는 알고리즘적으로 복잡한 문제다. 텐서 결과를 갖는 op가 주어지면, 버퍼화는 그 결과를 저장할 memref 버퍼를 선택해야 한다. 매번 새로운 버퍼를 할당하는 것은 항상 안전하지만, 고성능 코드 생성을 위해서는 그런 전략이 용납되기 어렵다. 기존 버퍼를 선택할 때에는 프로그램 후반에 여전히 필요한 데이터를 실수로 덮어쓰지 않도록 주의해야 한다.
이 문제를 단순화하기 위해, One-Shot Bufferize는 Destination-Passing Style(DPS)을 활용하도록 설계되었다. MLIR에서 DPS op는 DestinationStyleOpInterface를 구현해야 한다. DPS는 버퍼화와 독립적으로 존재하며 SSA 의미론에 밀접하다. 많은 op가 입력 SSA 값을 일부 “갱신”한다. 예를 들어 LLVM 명령어 insertelement는 벡터 내부에 원소를 삽입한다. SSA 값은 불변이므로, 이 연산은 삽입된 원소를 포함하는 입력 벡터의 사본을 반환한다. MLIR의 또 다른 예는 텐서에 대한 linalg.generic인데, 이 op는 항상 각 결과에 대해 추가적인 outs 피연산자를 가지며, 이는 갱신의 초기 값을 제공한다(예: 연산이 축약(reduction)을 수행할 때와 같이).
이후에서 outs 피연산자를 “목적지(destination)”라고 부르며(따옴표가 중요한데, 이 피연산자는 제자리로 수정되는 것이 아니라 복사되기 때문이다), 버퍼화 맥락에서 버퍼화 알고리즘의 가능한 “앵커”로 활용된다. 이는 사용자가 “목적지”로 사용할 SSA 값을 신중히 선택할 경우, 최적에 가까운 버퍼화 결과를 보장하는 형태로 입력을 구성할 수 있게 해준다.
각 텐서 결과에는 DPS op의 해당 텐서 피연산자가 있다. 이 텐서에 다른 충돌하는 사용이 없다면, 버퍼화는 해당 피연산자와 op 결과를 별칭(alias)시켜, 이 “목적지” 입력에 할당된 버퍼를 재사용하여 연산을 제자리로 수행할 수 있다.
예로 다음 op를 보자: %r = tensor.insert %f into %t[%idx] : tensor<5xf32>
이 예시에서 %t가 “목적지”다. 결과 %r의 버퍼(buffer(%r))를 선택할 때, One-Shot Bufferize는 다음 두 가지 선택지만 고려한다.
buffer(%r) = buffer(%t): 기존 buffer(%t)에 결과를 저장한다. 이는 항상 가능한 것은 아니다. 예를 들어, buffer(%t)의 이전 내용이 아직 필요하다면 불가능하다. One-Shot Bufferize의 주된 작업은 이러한 경우를 탐지하여 필요할 때 두 번째 선택지로 폴백하는 것이다.buffer(%r)는 새로 할당된 버퍼다.같은 함수 내에는 buffer(%r)로 사용할 수 있는 다른 버퍼도 있을 수 있지만, 버퍼화를 단순하게 유지하기 위해 One-Shot Bufferize는 이러한 버퍼를 고려하지 않는다. 향후 더 나은 버퍼화 품질을 위해 이러한 버퍼까지 고려하도록 확장될 수 있다.
Destination-Passing Style이 아닌 텐서 op는 항상 메모리 할당을 동반하는 방식으로 버퍼화된다. 예:
%0 = tensor.generate %sz {
^bb0(%i : index):
%cst = arith.constant 0.0 : f32
tensor.yield %cst : f32
} : tensor<?xf32>
tensor.generate의 결과에는 “목적지” 피연산자가 없으므로, 버퍼화는 새 버퍼를 할당한다. 대신 linalg.generic 같은 op를 사용하면, 동일한 계산을 “목적지” 피연산자(출력 outs 뒤에 지정)로 표현할 수 있어 이를 피할 수 있다.
#map = affine_map<(i) -> (i)>
%0 = linalg.generic {indexing_maps = [#map], iterator_types = ["parallel"]}
outs(%t : tensor<?xf32>) {
^bb0(%arg0 : f32):
%cst = arith.constant 0.0 : f32
linalg.yield %cst : f32
} -> tensor<?xf32>
겉보기에는 위의 linalg.generic op가 그다지 유용하지 않아 보일 수 있다. 출력 텐서 %t가 완전히 덮어쓰이기 때문이다. 그렇다면 처음부터 왜 %t를 피연산자로 전달하는가? 예를 들어 텐서의 슬라이스 일부만 덮어쓸 때 유용하다.
%t = tensor.extract_slice %s [%idx] [%sz] [1] : tensor<?xf32> to tensor<?xf32>
%0 = linalg.generic ... outs(%t) { ... } -> tensor<?xf32>
%1 = tensor.insert_slice %0 into %s [%idx] [%sz] [1]
: tensor<?xf32> into tensor<?xf32>
위 예시는 슬라이스 %t에 다른 사용자가 없다고 가정하면, memref.subview로 버퍼화된 다음, 그 서브뷰의 메모리를 덮어쓰는 “memref에 대한 linalg.generic”으로 이어진다. 이후 tensor.insert_slice는 %s의 후속 읽기 같은 RaW 충돌이 없는 경우 no-op으로 버퍼화된다.
RaW(Read-after-Write) 충돌은 SSA 정의-사용 체인 분석으로 탐지한다(자세한 내용은 후술). 다음과 같이 텐서 op의 결과가 다음 텐서 op의 피연산자가 되는 단일 SSA 정의-사용 체인으로 구성된 경우, One-Shot Bufferize가 가장 잘 동작한다.
%0 = "my_dialect.some_op"(%t) : (tensor<?xf32>) -> (tensor<?xf32>)
%1 = "my_dialect.another_op"(%0) : (tensor<?xf32>) -> (tensor<?xf32>)
%2 = "my_dialect.yet_another_op"(%1) : (tensor<?xf32>) -> (tensor<?xf32>)
정의-사용 체인이 어느 시점에 분기하면 버퍼 복사가 삽입될 가능성이 높다. 예:
%0 = "my_dialect.some_op"(%t) : (tensor<?xf32>) -> (tensor<?xf32>)
%1 = "my_dialect.another_op"(%0) : (tensor<?xf32>) -> (tensor<?xf32>)
// "yet_another_op"은 아마 %0의 데이터를 읽어야 하므로,
// "another_op"는 buffer(%0)에 제자리로 쓸 수 없다.
%2 = "my_dialect.yet_another_op"(%0) : (tensor<?xf32>) -> (tensor<?xf32>)
버퍼화 다이얼렉트는 텐서 IR(버퍼화 대상)과 기존 버퍼(다른 런타임/라이브러리/등에 의해 할당/제공될 수 있음)를 연결하는 데 도움이 되는 몇 가지 보조 op를 제공한다.
bufferization.to_buffer %t는 텐서 SSA 값의 미래 버퍼를 반환한다. bufferization.to_tensor %m는 주어진 MemRef 버퍼에 대한 텐서 SSA 값을 반환한다. bufferization.materialize_in_destination는 특정 버퍼 안에서 텐서 값이 실체화되어야 함을 나타낸다.
다음 예시를 보자. TOSA matmul 결과가 기존 버퍼 %C 안에서 실체화되어야 하는 경우다.
// 배치된 TOSA 행렬 곱. %A와 %B는 입력, %C는 출력이다.
func.func @test_matmul(%A: memref<1x17x19xf32>,
%B: memref<1x19x29xf32>,
%C: memref<1x17x29xf32>) {
%A_tensor = bufferization.to_tensor %A restrict : memref<1x17x19xf32> to tensor<1x17x19xf32>
%B_tensor = bufferization.to_tensor %B restrict : memref<1x19x29xf32> to tensor<1x19x29xf32>
%0 = tosa.matmul %A_tensor, %B_tensor
: (tensor<1x17x19xf32>, tensor<1x19x29xf32>) ->
tensor<1x17x29xf32>
bufferization.materialize_in_destination
%0 in restrict writable %C
: (tensor<1x17x29xf32>, memref<1x17x29xf32>) -> ()
return
}
위 예시의 모든 버퍼화 op에는 restrict 단위 속성이 설정되어 있음을 주의하라. 이 속성은 C의 restrict 키워드와 유사하며, 동일하거나 별칭 관계인 MemRef 피연산자를 갖는 다른 to_tensor 또는 materialize_in_destination op가 존재하지 않음을 나타낸다. 이러한 restrict가 있는 to_tensor/materialize_in_destination op만 지원된다. restrict 속성은 버퍼화 분석에 강력한 별칭 보장을 제공하여, 프로그램에서 텐서 IR만 살펴보도록 할 수 있게 한다(텐서에 작동하지 않는 op는 One-Shot Bufferize가 무시한다).
또한 tosa.matmul은 그 자체로는 버퍼화할 수 없음을 주의하라. 해당 op에 대한 BufferizableOpInterface 구현이 없기 때문이다. 하지만 이 op는 tensor.empty와 linalg.matmul의 조합으로 내릴 수 있으며, 이 조합은 버퍼화할 수 있다.
MLIR은 BufferizableOpInterface를 구현하는, 텐서 의미론을 갖는 모든 op에 대해 분석을 수행하고 버퍼화하는 -one-shot-bufferize 패스를 제공한다. 모듈성 때문에, 이러한 op 인터페이스 구현은 보통 다이얼렉트의 “Transforms” 빌드 유닛에 존재하는 외부 모델(external model)이다. (외부 모델은 다른 빌드 유닛에서 op 인터페이스를 구현하는 메커니즘이다.) One-Shot Bufferize를 실행하기 전에 필요한 모든 외부 모델이 등록되어 있는지 확인하는 것은 사용자 책임이다.
기본적으로, 버퍼화할 수 없는(즉, BufferizableOpInterface를 구현하지 않은) 텐서 의미론 op(텐서 결과나 텐서 피연산자를 가지는 op)를 만나면 One-Shot Bufferize는 실패한다. 이는 allow-unknown-ops로 피할 수 있다. 이 경우, One-Shot Bufferize는 버퍼화 경계 주변에 to_buffer/to_tensor op를 삽입한다.
One-Shot Bufferize는 dialect-filter로 특정 다이얼렉트 집합에 속한 op만 버퍼화하도록 구성할 수 있다.
프로그래밍적으로는 bufferization::runOneShotBufferize를 호출하여 사용할 수 있다. 또는 분석을 건너뛰고 모든 버퍼 쓰기마다 복사를 삽입하는 bufferization::bufferizeOp를 사용할 수 있다.
기본적으로 함수 경계는 버퍼화하지 않는다. 현재 함수 그래프 버퍼화에는 제한이 있는데, 재귀 호출이 지원되지 않기 때문이다. 재귀 호출이 없다면 bufferize-function-boundaries로 함수 경계 버퍼화를 활성화할 수 있다. 그러면 각 텐서 함수 인자와 텐서 함수 결과가 memref로 바뀐다. memref 타입의 레이아웃 맵은 function-boundary-type-conversion으로 제어할 수 있다.
One-Shot Bufferize는 위에서 아래로(op 순서대로) 버퍼화한다. 모든 op가 버퍼화 가능할 때는 잘 동작한다. 하지만 allow-unknown-ops로 비버퍼화 텐서를 만나면, One-Shot Bufferize는 버퍼화 경계에 to_buffer op를 삽입해야 하며, 이때 memref 타입을 결정해야 한다. 기본적으로 One-Shot Bufferize는 레이아웃 맵 관점에서 가장 동적인 memref 타입을 선택한다. 예:
%0 = "my_dialect.unbufferizable_op(%t) : (tensor<?x?xf32>) -> (tensor<?x?xf32>)
%1 = tensor.extract %0[%idx1, %idx2] : tensor<?xf32>
위 IR을 버퍼화할 때, One-Shot Bufferize는 동적 오프셋과 스트라이드를 갖는 to_buffer op를 삽입한다.
%0 = "my_dialect.unbufferizable_op(%t) : (tensor<?x?xf32>) -> (tensor<?x?xf32>)
%0_m = bufferization.to_buffer %0 : memref<?x?xf32, strided<[?, ?], offset: ?>>
%1 = memref.load %0_m[%idx1, %idx2] : memref<?x?xf32, strided<[?, ?], offset: ?>>
%0의 모든 사용자는 완전히 동적인 레이아웃 맵을 갖는다. 이는 이후 unbufferizable_op가 어떻게 버퍼화되든(아마 다른 패스가 버퍼화) 정확한 memref 타입과 상관없이 버퍼화된 IR이 잘 합성되도록 보장한다. 해당 op가 더 단순한 memref 타입(예: 항등 레이아웃 맵)으로 버퍼화되는 것으로 드러나면, 정규화(canonicalization) 패턴이 불필요하게 동적인 레이아웃 맵을 정리해 줄 것으로 기대한다(일부 정규화 패턴은 아직 구현되지 않았을 수 있다).
One-Shot Bufferize는 op를 버퍼화할 때 가장 정밀한 memref 타입을 추론하려고 한다. 전체 IR이 버퍼화 가능하다면, 보수적으로 완전 동적 레이아웃 맵을 사용할 필요가 없다. 이 경우 버퍼화된 IR을 정리하기 위해 정규화 패턴에 의존할 필요도 없다.
참고: 일부 버퍼화 가능한 op의 경우 정확한 레이아웃 맵을 추론할 수 없다. 예를 들어, tensor<*xf32>에서 tensor<?x?xf32>로의 tensor.cast는 완전히 동적인 레이아웃 맵을 가진 memref 타입의 memref.cast로 버퍼화되어야 한다.
One-Shot Bufferize에는 정확한 레이아웃을 추론할 수 없을 때 레이아웃 맵 생성 방식을 제어하는 unknown-type-conversion 옵션이 있다.
fully-dynamic-layout-map은 완전 동적 레이아웃 맵을 사용하며 기본 동작이다. 부분적으로 버퍼화된 IR과 잘 합성된다.identity-layout-map은 정적 항등 레이아웃 맵을 사용한다. 레이아웃 맵이 있는 memref 타입을 처리하지 못하는 레거시 코드에는 유용할 수 있다. 이 설정은 캐스트 호환되지 않는 memref 타입의 to_tensor/to_buffer 쌍을 접을 때 추가 버퍼 복사를 유발할 수 있음을 유의하라.참고: unknown-type-conversion 옵션은 함수 시그니처의 레이아웃 맵에는 영향을 주지 않는다. 함수 매개변수와 함수 결과의 레이아웃 맵은 별도의 function-signature-type-conversion 옵션으로 제어한다.
사용자 정의 op는 BufferizableOpInterface를 구현하면 버퍼화할 수 있다. 최소한 다음 인터페이스 메서드를 구현해야 한다.
bufferizesToMemoryRead: 주어진 텐서 OpOperand의 버퍼가 읽히는 경우 true를 반환한다.bufferizesToMemoryWrite: 주어진 텐서 OpOperand의 버퍼가(제자리 버퍼화 시) 쓰이는 경우 true를 반환한다.getAliasingOpResult: 주어진 OpOperand와 동일한 버퍼를 공유할 수 있는 OpResult를 반환한다. 이 메서드는 Destination-Passing Style 관점에서 OpOperand와 OpResult 간 매핑을 기술한다.bufferRelation: 주어진 OpResult가 버퍼화 후 별칭하는 OpOperand와 정확히 같은 memref(제자리 버퍼화의 경우)라면 BufferRelation::Equivalent를 반환한다. 그렇지 않다면(예: 겹치지만 정확히 같은 memref라는 보장은 없을 때) BufferRelation::Unknown을 반환해야 한다. 앞으로 추가적인 관계가 더해질 수 있으나, BufferRelation::Unknown은 항상 안전하다.bufferize: 제공된 rewriter를 사용해 op를 다시 쓴다. op는 bufferization::replaceOpWithBufferizedValues로 대체해야 한다.인터페이스 메서드에 대한 감을 잡으려면, MLIR의 기존 구현(예: tensor.insert나 tensor.extract 구현)을 참고하길 권장한다.
DestinationStyleOpInterface를 구현하는 DPS op의 인터페이스 구현은 DstBufferizableOpInterfaceExternalModel에서 파생될 수 있으며, bufferize를 제외한 필요한 메서드 구현을 모두 제공한다.
One-Shot Bufferize가 버퍼 복사를 도입한 이유를 더 잘 이해하려면, 패스를 test-analysis-only print-conflicts와 함께 실행해 보라. 그러면 모든 텐서 op에 각 텐서 OpOperand에 대한 불리언 값을 담는 속성이 주석으로 붙는다. true는 해당 OpOperand가 제자리로 버퍼화됨을 의미한다. false는 제자리가 아니며 버퍼 복사가 삽입됨을 의미한다.
버퍼 복사가 삽입되는 이유는 두 가지다.
arith.constant op의 결과인 memref.global 버퍼는 절대 수정되지 않는다.첫 번째 경우, print-conflicts는 (“읽기”, “충돌하는 쓰기”, “마지막 쓰기”) 튜플 형태로 충돌을 보여준다.
RaW 충돌은 다음 세 부분으로 구성되며, op 지배(dominance) 순서는 다음과 같다.
%t가 정의된다.buffer(%t)에 쓴다.%t를 읽는다.분석 단계에서 이러한 RaW 충돌이 탐지되면, One-Shot Bufferize는 충돌하는 쓰기에 대해 버퍼 복사를 삽입한다.
예시
// RUN: mlir-opt %s -one-shot-bufferize="bufferize-function-boundaries test-analysis-only print-conflicts"
func.func @test(%arg0: f32, %arg1: f32, %arg2: index, %arg3: index) -> (f32, tensor<3xf32>) {
// [%arg0, %arg0, %arg0]으로 구성된 새 텐서를 만든다.
%0 = tensor.from_elements %arg0, %arg0, %arg0 : tensor<3xf32>
// 새 텐서에 어떤 값을 삽입한다.
%1 = tensor.insert %arg1 into %0[%arg2] : tensor<3xf32>
// 이전 텐서에서 읽는다.
%r = tensor.extract %0[%arg3] : tensor<3xf32>
// 추출한 값과 삽입 결과를 반환한다.
func.return %r, %1 : f32, tensor<3xf32>
}
출력 IR은 다음과 같다.
func.func @test(%arg0: f32, %arg1: f32, %arg2: index, %arg3: index) -> (f32, tensor<3xf32>) {
%from_elements = tensor.from_elements %arg0, %arg0, %arg0 {"C_0[DEF: result 0]"} : tensor<3xf32>
%inserted = tensor.insert %arg1 into %from_elements[%arg2] {"C_0[CONFL-WRITE: 1]", __inplace_operands_attr__ = ["none", "false", "none"]} : tensor<3xf32>
%extracted = tensor.extract %from_elements[%arg3] {"C_0[READ: 0]", __inplace_operands_attr__ = ["true", "none"]} : tensor<3xf32>
return {__inplace_operands_attr__ = ["none", "true"]} %extracted, %inserted : f32, tensor<3xf32>
}
이 IR은 버퍼화된 것이 아니라, 버퍼화 분석 결과만 주석으로 달렸음을 주의하라. 텐서 의미론을 가진 모든 연산에는 각 피연산자마다 한 값이 있는 __inplace_operands_attr__ 속성이 있다. 피연산자가 텐서가 아니면 해당 값은 none이다. 피연산자가 제자리 버퍼화되기로 결정되면 값은 true다. false 값은 버퍼 복사를 의미한다. 위 예시에서는 tensor.insert가 buffer(%from_elements)를 덮어쓰지 않도록 버퍼 복사가 삽입되는데, 이는 tensor.extract에서 여전히 필요하기 때문이다.
각 RaW(예시에서는 하나뿐이다)에 대해 세 개의 C_i 속성이 추가되었다.
C_0[DEF: result 0]: 텐서가 정의됨: tensor.from_elements의 0번째 결과.C_0[CONFL-WRITE: 1]: 어떤 연산이(제자리 버퍼화될 경우) 정의된 텐서의 미래 버퍼에 씀: tensor.insert의 1번째 피연산자.C_0[READ: 0]: 어떤 연산이 해당 정의를 읽음: tensor.extract의 0번째 피연산자.삽입된 버퍼 복사를 포함한 완전 버퍼화된 IR은 다음과 같다.
func.func @test(%arg0: f32, %arg1: f32, %arg2: index, %arg3: index) -> (f32, memref<3xf32>) {
%c2 = arith.constant 2 : index
%c1 = arith.constant 1 : index
%c0 = arith.constant 0 : index
%alloc = memref.alloc() {alignment = 64 : i64} : memref<3xf32>
memref.store %arg0, %alloc[%c0] : memref<3xf32>
memref.store %arg0, %alloc[%c1] : memref<3xf32>
memref.store %arg0, %alloc[%c2] : memref<3xf32>
%alloc_0 = memref.alloc() {alignment = 64 : i64} : memref<3xf32>
memref.copy %alloc, %alloc_0 : memref<3xf32> to memref<3xf32>
memref.store %arg1, %alloc_0[%arg2] : memref<3xf32>
%0 = memref.load %alloc[%arg3] : memref<3xf32>
return %0, %alloc_0 : f32, memref<3xf32>
}
SSA 정의-사용 체인 분석과 RaW 충돌 탐지 알고리즘을 더 잘 이해하고 싶다면 다음 자료를 참고하라.