- Fedora와 Ubuntu 24.04 LTS가 libc 등 OS 구성요소를 기본적으로 프레임 포인터 포함 빌드로 되돌리면서, Linux 프로파일링과 Flame Graph가 더 완전한 스택을 볼 수 있게 됨
- 프레임 포인터가 없으면 profiler가 libc 같은 라이브러리 계층에서 스택 워킹을 멈추거나 잘못된 프레임을 따라가 CPU·Off-CPU Flame Graph가 깨질 수 있음
- 2004년 gcc의 i386 변경은 레지스터를 하나 더 확보하려는 선택이었지만, x86-64까지 퍼지며 시스템 profiler와 eBPF 기반 관측성에 장기적인 비용을 남김
- Netflix의 Java와 libc 적용 경험에서 비용은 보통 1% 미만이었고, 일부 보고는 1~2%, 특수한 마이크로벤치마크나 비정상 워크로드에서는 10%까지 나타남
- LBR, DWARF, eBPF 스택 워커, ORC, SFrames, Shadow Stacks 같은 대안은 있지만, 지금 프로덕션에서 성능 개선을 찾는 가장 실용적인 방법은 프레임 포인터 기본 활성화임
Flame Graph가 깨지는 이유
- CPU Flame Graph는 겉보기에는 정상처럼 보여도, libc가 프레임 포인터 없이 컴파일된 시스템에서는 일부 샘플이 [unknown] 위에 잘못 모일 수 있음
- 예시에서는 왼쪽의 15% 샘플이 잘못된 위치에 있으며 애플리케이션 프레임이 빠져 있음
- profiler가 커널 프레임을 지나 syscall과 libc syscall wrapper까지 도달한 뒤 다음 프레임 심볼 해석에 실패함
- 원인은 컴파일러 최적화로 프레임 포인터 레지스터가 스택 프레임 기준점이 아니라 데이터 저장에 쓰이기 때문임
- profiler는 해당 값이 단순 숫자인지 알 수 없어 함수 주소로 해석하려고 시도함
- 값이 다음 프레임을 가리키지 않으면 스택 워킹이 중단됨
- 우연히 유효한 포인터처럼 보이면 잘못된 junk frame이 생길 수 있음
- 값이 자기 자신을 가리키는 경우 perf의 최대 프레임 제한에 닿을 때까지 junk frame 탑이 생길 수 있음
- Off-CPU Flame Graph는 libc read/write와 mutex 함수가 많이 등장할 수 있어 프레임 포인터가 없을 때 더 크게 깨짐
- 애플리케이션 자체도 프레임 포인터 없이 빌드되어 있으면 라이브러리뿐 아니라 전체 스택 추적이 흔들림
프레임 포인터가 제공하는 정보
- x86-64 ABI는
%rbp를 스택 프레임의 base pointer, 즉 프레임 포인터로 사용할 수 있다고 정의함 - Linux perf와 eBPF 같은 외부 profiler·debugger는 이 정보를 이용해 스택 추적을 걷고, 결과를 Flame Graph로 시각화함
- x86-64 ABI에는
%rbp사용이 선택 사항이라는 각주가 있음%rsp로 스택 프레임을 인덱싱하면 prologue와 epilogue에서 두 개의 명령어를 줄일 수 있음%rbp를 추가 범용 레지스터로 사용할 수 있음
2004년 제거와 20년의 여파
- 2004년 gcc는 i386 backend에서 기본적으로
-fomit-frame-pointer -ffixed-ebp에 해당하는 동작을 하도록 바뀜 - i386은 범용 레지스터가 4개뿐이어서
%ebp를 해제하면 사용 가능한 레지스터가 크게 늘어남- 변경 이유에는 Intel의
icc컴파일러보다 나은 성능을 내고 싶다는 점도 포함됨 - 당시에는 debugger가 다른 스택 워킹 기법을 지원하므로 깨지지 않는다는 판단도 있었음
- 변경 이유에는 Intel의
- 이 변경은 x86-64에도 적용됨
- x86-64는 레지스터가 12개 이상이어서 레지스터 하나를 더 얻는 이점이 i386만큼 크지 않음
- eBPF 같은 오늘날의 시스템 profiler는 당시 존재하지 않았고, 이 변경으로 깨지는 경우가 있음
- Eric Schrock은 2004년에 amd64에서 17번째 범용 레지스터를 얻는 이점이 디버깅 가능성 손실에 비해 충분하지 않다고 봄
/usr/bin을 프레임 포인터 없이 컴파일하기 시작하면 통제 불능이 된다는 평가를 남김- Linux에서는
/usr/bin뿐 아니라/usr/lib와 애플리케이션 코드까지 같은 흐름이 확산됨
Java, libc, Netflix에서의 적용 경험
- Netflix에 합류한 2014년에는 Java의 프레임 포인터 지원 부족으로 애플리케이션 스택이 모두 깨졌음
- JVM c2 compiler용 수정이 만들어졌고, Oracle이 이를 재작업해 JDK8u60의
-XX:+PreserveFramePointer옵션으로 추가함 - Java 변경으로 애플리케이션 코드의 여러 성능 개선을 찾을 수 있었지만, libc는 여전히 일부 CPU 샘플과 대부분의 Off-CPU Flame Graph를 깨뜨렸음
- 이후 자체적으로 프레임 포인터가 포함된 libc를 프로덕션용으로 컴파일했고, Canonical과 함께 Ubuntu용 사전 빌드 libc 작업도 진행함
- 한동안
libc6-prof사용이 권장됨 libc6-prof는 프레임 포인터가 포함된libc6였음
- 한동안
성능 오버헤드와 예외 사례
- 프로덕션 적용 과정에서 프레임 포인터를 libc와 Java에 추가한 비용은 보통 1% 미만이었음
- 예외적으로 10% 오버헤드가 나온 애플리케이션이 있었음
- Groovy를 통해 1000프레임이 넘는 스택 추적을 생성하던 특이한 애플리케이션이었음
- Linux perf도 이를 처리하지 못해 Red Hat의 Arnaldo Carvalho de Melo가 Netflix 워크로드를 위해
kernel.perf_event_max_stacksysctl을 추가함 - 해당 환경은 저수준 하드웨어 profiling 기능이 없는 가상머신이라 10%가 전부 프레임 포인터 때문인지 확인할 수 없었음
- 다른 보고에서는 약 1% 또는 2% 수준의 비용도 나타남
- 마이크로벤치마크는 10%까지 나빠질 수 있음
- 작은 함수를 루프에서 반복 실행할 때 추가 명령어가 L1 cache warmth나 cache line 경계를 건드릴 수 있음
- 이 경우 프레임 포인터 자체보다 hot 함수에 무엇이든 추가되면 같은 효과가 날 수 있음
- Python
scimark_sparse_mat_mult벤치마크도 10%에 이를 수 있었음- Andrii Nakryiko의 분석에서는 큰 함수에서 gcc가
%rspoffset 대신%rbprelative offset을 사용하며 더 많은 바이트가 필요해 성능 문제가 생긴 특수 사례였음 - 이후 Python이 프레임 포인터를 기본 재활성화할 수 있도록 수정됐다는 소식도 있었음
- Andrii Nakryiko의 분석에서는 큰 함수에서 gcc가
- 프레임 포인터로 찾은 성능 개선은 5%에서 500%까지 있었고, 보통 1% 미만 비용은 정당화 가능하다고 봄
- profiling이나 debugging 가능성이 없는 기기라면 프레임 포인터 없이 컴파일해도 되지만, 주요 대상은 엔터프라이즈 Linux와 백엔드 서버임
Fedora와 Ubuntu의 기본 활성화
- Meta, Google, Netflix 같은 대기업은 이미 자체적으로 프레임 포인터가 포함된 libc를 사용해 profiling 기능을 확보하고 있었음
- Fedora에 기본 활성화를 업스트림하려는 첫 시도는 긴 논쟁으로 이어짐
- Fedora 논의는 116개 글이 달린 스레드가 됨
- 한 참여자는 Meta나 Netflix가 테스트·벤치마크·코드 크기 측정을 위한 side repository 인프라를 제공해야 한다고 요구함
- Jonathan Corbet은 이를 Fedora's tempest in a stack frame에서 정리함
- Fedora는 이후 제안을 다시 검토해 받아들였고, 프레임 포인터를 재활성화한 첫 배포판이 됨
- Ubuntu도 Ubuntu 24.04 LTS에서 frame pointers by default를 발표함
- Arch Linux도 프레임 포인터 활성화를 진행 중이라는 소식이 추가됨
- OS 라이브러리 스택 워킹은 이 변화로 개선되지만, 애플리케이션 런타임은 별도 설정이 필요할 수 있음
- Java는
-XX:+PreserveFramePointer옵션을 제공함 - Go는 프레임 포인터 지원이 몇 년 전 기본값이 됨
- Java는
프레임 포인터 이후의 스택 워킹 후보
- LBR(Last Branch Record): Intel 하드웨어 기능으로 16 또는 32프레임 제한이 있어 대부분의 애플리케이션 스택에는 부족하지만, 일부 스택 정보를 얻는 마지막 수단으로 쓸 수 있음
- BTS(Branch Trace Store): Intel 기능으로 스택 깊이 제한은 덜하지만 메모리 load/store와 BTS buffer overflow interrupt 처리 비용이 있음
- AET(Architectural Event Trace): JTAG 기반 tracer로 저수준 CPU, BIOS, device event를 추적할 수 있고 stack trace에도 쓸 수 있는 것으로 보이지만 직접 사용한 경험은 없음
- DWARF: debugger에서 오래 쓰인 binary debuginfo 방식임
- JIT 런타임에서의 JIT-to-DWARF 작업이 있었다는 지적이 있었음
- 바쁜 프로덕션 서버에서 c2가 계속 도는 Java JVM에는 실용적일 것으로 기대하지 않음
- DWARF 스택 워킹 자체의 비용도 높음
- eBPF stack walking: 외부 tracer가 런타임 지원 없이 JVM 같은 런타임 내부를 걸을 수 있음
- 런타임 내부를 user space read로 많이 읽어야 할 수 있어 오버헤드가 높을 수 있음
- 런타임 변화에 취약하므로 language code base와 함께 배포·유지되는 편이 적절함
- ORC(oops rewind capability): Linux kernel의 경량 stack unwinder로, 새 kernel이 프레임 포인터를 제거하면서도 stack walking을 유지할 수 있게 함
- SFrames(Stack Frames): ORC 기반의 경량 사용자 스택 unwinding 방식임
- Shadow Stacks: Intel과 AMD의 보안 기능으로, 함수 return address를 별도 하드웨어 스택에 push해 return 시점에 검증할 수 있으며 stack trace에도 활용 가능해 보임
지금 당장 달라지는 점
- 2004년 프레임 포인터 생략의 이유였던 i386 성능 이점, 당시 debugger 호환성 판단,
icc와의 경쟁은 2024년 상황과 맞지 않음 - x86-64에 적용된 프레임 포인터 제거는 당시에도 디버깅 가능성 손실에 비해 이점이 충분하지 않다는 평가가 있었음
- Fedora와 Ubuntu가 프레임 포인터를 되돌리면서 2024년 릴리스를 쓰는 사람들은 CPU Flame Graph를 더 이해하기 쉬워지고 Off-CPU Flame Graph를 처음으로 제대로 활용할 수 있음
- continuous profiler도 고객에게 OS 변경을 요구하지 않고 더 완전한 프로파일을 얻기 쉬워짐
- 미래에는 SFrames나 Shadow Stacks 같은 방식으로 다시 프레임 포인터 없이 스택 추적을 할 수 있을지 모르지만, 지금 가능한 개선은 프레임 포인터를 기본값으로 켜는 것임