Lobsters

Chasing a -O2 pessimization in antcc

antcc의 -O2 성능 저하를 추적하다

C 컴파일러 antcc에서 Lua 벤치마크가 -O2로 더 느려지는 원인을 추적합니다. mem2reg가 루프 안의 초기화되지 않은 지역 변수를 잘못 처리해 불필요한 phi 노드를 만들었고, 레지스터 압박과 스필링이 늘어났습니다. alloca 처리 한 줄을 고쳐 성능 저하가 사라졌습니다.

에디터 노트

불필요한 phi 노드가 레지스터 스필링을 늘렸다는 원인 추적은 IR의 크기 변화와 최종 명령어 구성을 연결해 설명합니다. 특히 코드 크기보다 레지스터 할당 단계의 팽창이 문제였다는 점을 구체적인 수치로 보여줍니다. 다만 수정 후 벤치마크에는 switch 코드 생성 개선과 다른 최적화도 함께 들어갔습니다. 따라서 mem2reg 수정이 제거한 성능 저하와 전체 벤치마크의 개선 폭은 구분해야 합니다. 글은 별도로 제시한 초기 측정치와 IR 분석으로 컴파일러 내부의 원인을 뒷받침하지만, 후속 수치는 수정 하나만의 효과를 측정한 값이 아닙니다.

AI 요약

antcc로 Lua 5.4.7을 컴파일해 벤치마크하자 최적화 옵션이 오히려 실행 시간을 늘렸습니다. 기본 빌드는 1.048초였고 -O1은 1.077초, -O2는 1.410초였습니다. -O2에서 Lua VM의 바이트코드 해석 루프인 luaV_execute가 약 43KB로 커졌습니다. -O1보다 2.4배 큰 크기입니다. 인라이닝을 끄면 성능 저하가 사라져 코드 팽창과 명령어 캐시 문제를 의심했지만, 인라이닝 임계값 조정만으로는 원인을 찾지 못했습니다.

IR 크기보다 커진 기계어

최적화가 끝난 중간 표현(IR)은 -O2가 -O1보다 11% 컸습니다. 하지만 레지스터 할당 뒤에는 크기 차이가 70%까지 벌어졌고, luaV_execute 명령어 중 40%가 스택과 레지스터 사이에서 값을 옮기는 mov였습니다. antcc는 가상 레지스터를 실제 CPU 레지스터에 배치하는 선형 스캔 방식의 레지스터 할당을 씁니다. 사용할 레지스터가 부족하면 값을 스택에 저장했다가 다시 불러와야 하는데, 이 스필링 작업이 불필요한 명령어를 대량으로 만들었습니다.

루프를 가로지른 phi 노드

원인은 메모리에 놓인 지역 변수를 SSA 형태로 바꾸는 mem2reg 패스에서 드러났습니다. SSA에서는 분기마다 값이 달라지는 변수를 phi 노드로 합칩니다. 루프 안의 값은 다음 반복에서도 쓰이므로 루프 헤더의 phi 노드가 루프 전체에 걸쳐 살아 있을 수 있습니다. antcc의 IR에는 Lua 해석 루프 시작 부분에 phi 노드 16개가 생겨 있었습니다. 이 값들이 레지스터 압박을 일으켰습니다.

Lua 코드에서 정수 변환 함수는 변환에 성공했을 때만 출력 인자를 설정합니다. 그 함수가 인라인되면 antcc의 mem2reg는 변환 성공 여부를 검사하는 분기와 출력 변수의 초기화를 서로 연결하지 못합니다. 따라서 성공 분기에서만 값이 쓰이는 변수를 초기화되지 않은 값과 합치는 phi 노드를 만들었습니다. 문제는 이 지역 변수가 루프 안의 특정 분기에서만 쓰이는데도, alloca를 만났을 때 현재 블록의 정의를 기록하지 않아 변수 정의를 찾는 과정이 루프의 역방향 간선까지 거슬러 올라갔다는 점입니다. 그 결과 지역 변수가 이전 반복의 값과 이어지는 것처럼 보이는 phi 노드가 생겼습니다.

한 줄 수정과 남은 경우

작성자는 alloca 처리 시 현재 블록의 변수 정의를 UNDREF로 초기화했습니다. 그러면 정의를 찾는 재귀 탐색이 루프 역방향 간선까지 넘어가지 않아 해당 phi 노드가 생기지 않습니다. 이 수정 뒤 문제로 지목한 성능 저하가 사라졌습니다. 다만 함수 시작 부분에서 모든 변수를 선언하는 오래된 C 코드에서는 비슷한 문제가 남을 수 있습니다. 작성자는 Tarjan 알고리즘으로 phi 데이터 흐름의 강결합 요소를 찾아 중복 phi를 판별하는 방법을 언급했지만 아직 구현하지 않았습니다. 인라인된 함수의 분기 조건을 대상으로 하는 phi 인식 점프 스레딩도 대안으로 제시합니다.

작성자는 벤치마크 환경이 Lua 5.4.7, bench.lua, AMD Ryzen 5 5600G라고 밝혔으며 수치는 참고용으로 봐 달라고 덧붙였습니다. 수정 후 수치에는 switch 문을 선형 조건 분기 대신 이진 탐색으로 처리하는 코드 생성과 몇 가지 최적화도 포함됐습니다. 따라서 수정 후의 전체 성능 향상을 mem2reg 수정 하나의 효과로 분리해 읽을 수는 없습니다. AI 에이전트는 역어셈블리와 IR 조사, 측정을 줄이는 데 도움을 줬지만 원인을 찾는 과정에서는 복잡한 수정안을 약 10개 제안했고, 최종 원인은 작성자가 직접 찾아냈다고 설명합니다.

원문: 386s.org / 번역·요약: Trawling