3P by GN⁺ | ★ favorite | 댓글 1개
  • 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가 다른 스택 워킹 기법을 지원하므로 깨지지 않는다는 판단도 있었음
  • 이 변경은 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_stack sysctl을 추가함
    • 해당 환경은 저수준 하드웨어 profiling 기능이 없는 가상머신이라 10%가 전부 프레임 포인터 때문인지 확인할 수 없었음
  • 다른 보고에서는 약 1% 또는 2% 수준의 비용도 나타남
  • 마이크로벤치마크는 10%까지 나빠질 수 있음
    • 작은 함수를 루프에서 반복 실행할 때 추가 명령어가 L1 cache warmth나 cache line 경계를 건드릴 수 있음
    • 이 경우 프레임 포인터 자체보다 hot 함수에 무엇이든 추가되면 같은 효과가 날 수 있음
  • Python scimark_sparse_mat_mult 벤치마크도 10%에 이를 수 있었음
    • Andrii Nakryiko의 분석에서는 큰 함수에서 gcc가 %rsp offset 대신 %rbp relative offset을 사용하며 더 많은 바이트가 필요해 성능 문제가 생긴 특수 사례였음
    • 이후 Python이 프레임 포인터를 기본 재활성화할 수 있도록 수정됐다는 소식도 있었음
  • 프레임 포인터로 찾은 성능 개선은 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는 프레임 포인터 지원이 몇 년 전 기본값이 됨

프레임 포인터 이후의 스택 워킹 후보

  • 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 같은 방식으로 다시 프레임 포인터 없이 스택 추적을 할 수 있을지 모르지만, 지금 가능한 개선은 프레임 포인터를 기본값으로 켜는 것임

댓글과 토론

Hacker News 의견들
  • 2000년대 초 스택 프레임 포인터 생략이 퍼지기 시작하던 때가 기억남
    당시 가난한 제3세계 국가의 대학에서 컴퓨터과학을 공부했고, 낡고 느린 컴퓨터 때문에 과제 대부분은 인터프리터보다 컴파일러를 썼음
    저수준 자료구조, 컴파일러, 어셈블리 수치 루틴, Minix 장치 드라이버까지 구현하는 흥미로운 수업이 많았고, 프로그램이 이상하게 동작하면 gdb로 붙어 어셈블리 수준에서 직접 스택을 따라가며 디버깅했음
    그런데 갑자기 -fomit-frame-pointer가 유행하면서 스택 추적이 말이 안 되기 시작했고, 세그폴트나 illegal instruction 디버깅이 훨씬 어려워졌음
    결국 깨진 디버깅 세션을 피하려고 거의 모든 일에 Python을 쓰게 됐고, 성능은 한두 자릿수 잃었지만 Python을 배운 건 이후에 도움이 됨

    • -fno-omit-frame-pointer를 몰랐던 건지 궁금함
  • Fedora가 언급돼서 반가움. 배포판 전체에서 프레임 포인터를 켜 둔 상태로 유지하는 건 꽤 지치는 싸움이었음
    예: https://pagure.io/fesco/issue/3084
    프레임 포인터 오버헤드가 크다는 신화가 계속 있는데, 이는 한 Python 사례에서 +10% 느려졌던 것 때문이고 지금은 수정됨
    실제 측정된 오버헤드는 1% 미만이며, 특정 애플리케이션에서 얻은 이점이 훨씬 큼

    • “실제 측정 오버헤드가 1% 미만”이라는 말은 과장으로 보임. 오버헤드는 측정하는 작업부하에 크게 의존하므로 보편적으로 적용하기 어렵다
      Linux 커널 쪽 측정과도 잘 맞지 않는데, 그쪽은 5~10% 범위라고 봤음: https://lore.kernel.org/lkml/20170602104048.jkkzssljsompjdwy...
      netperf, 페이지 할당자 마이크로벤치마크, pgbench, sqlite 같은 다양한 작업부하에서 프레임 포인터를 켜면 5~10% 오버헤드가 났고, PostgreSQL과 SQLite로 영향을 봤다는 점이 중요함
      DBMS는 시스템을 강하게 압박하는 좋은 방식 중 하나임
    • OCaml 5에서 플레임 그래프를 제대로 쓰려면 프레임 포인터를 쓰거나, 깊이가 제한적이고 CPU에 따라 안 될 수 있는 LBR을 쓰거나, OCaml의 두 스택을 처리하도록 perf/eBPF 쪽을 크게 바꿔야 함
      OCaml 5는 OCaml 코드와 C 코드에 별도 스택을 쓰며, GDB는 DWARF 정보로 둘을 연결할 수 있지만 perf의 DWARF 호출 그래프는 못 함: https://github.com/ocaml/ocaml/issues/12563#issuecomment-193...
      향후 릴리스에서도 프레임 포인터를 유지해야 할 근거가 필요하다면 OCaml 5를 예로 들 수 있음
      Fedora 39에서 이미 기본으로 프레임 포인터를 켰다는 건 이제 알았고, 평소 프로파일링은 아직 perf record --call-graph dwarf -F 47 -a를 쓰는 CentOS 7 비슷한 시스템에서 주로 하고 있었음
    • 32비트에서는 프레임 포인터가 여전히 곤란해서, 오늘날 IoT 같은 영역에는 맞지 않음
      프레임 포인터를 제거한 이유는 신화가 아니라 64비트 이전 시절의 현실에서 왔고, 그렇게 오래된 일도 아님
      지금도 오래된 64비트 시스템에 새 생명을 주려면 이런 최적화가 의미 있음
      이상적으로는 보안이 중요한 시스템에서도 기본값이어야 하며, 모든 것이 “관측 가능성”을 위해 최적화될 필요는 없음
    • “1% 미만” 오버헤드들이 쌓여서 컴퓨터 사용감이 30년 전보다 측정 가능하게 느려졌다고 봄. 그런 작은 오버헤드는 결국 누적됨
  • Apple이 ARM에서 잘한 점이 하나 있는데, x29 프레임 포인터가 항상 유효한 프레임 레코드를 가리키도록 한 것임
    leaf 함수나 tail call 같은 일부 함수는 목록에 항목을 만들지 않을 수 있지만, 그 결과 디버그 정보가 없어도 스택 추적이 항상 의미 있게 남음
    https://developer.apple.com/documentation/xcode/writing-arm6...

    • Apple 플랫폼에는 다른 종류의 해석 가능성 문제가 자주 있음. Objective-C / Swift 앱은 깊게 중첩된 블록과 클로저가 많아서 역추적이 여러 스레드에 흩어지는 일이 흔하고, 아직 좋은 해법을 모르겠음
  • 2005년에 Google에서 반대편 입장에 있었고, 당시 생각은 단순했음
    $BIG_COMPANY가 모든 것을 프레임 포인터로 컴파일하기로 해도 나머지 커뮤니티는 그렇게 하지 않을 테니, 훨씬 큰 커뮤니티와 이길 수 없는 논쟁을 계속하게 된다고 봤음
    결과적으로 그 논쟁은 거의 20년짜리 논쟁이 됐고, 이후 gperftools에서 libunwind가 동작하도록 패치를 작성하다가 몇 년간 libunwind를 유지하게 됨
    이제는 다른 컴퓨팅 분야로 옮겨 수동적인 관찰자가 됐지만, 반대편 관점에서 역사를 읽는 게 흥미로움

    • 프레임 포인터를 추가하면 어떤 식으로 발목이 잡히는지 궁금함. 기능적으로 어떤 문제가 생기는지 모르겠음
  • RBP를 프레임 포인터로 넘겨줄 거라면 차라리 스택을 두 개 두는 것도 가능함
    하나는 RBP가 가리키며 활성화 프레임을 저장하고, 다른 하나는 RSP가 가리키며 반환 주소만 저장하는 식임
    그러면 호출 스택은 말 그대로 반환 주소의 평평한 배열이므로 “스택을 걷는” 일도 필요 없어짐
    애초에 반환 주소를 지역 변수 가까이에 저장하는 이유가 뭔지 모르겠고, 단점이 너무 많아 보임

    • 저장소 관리가 단순해지기 때문임. 스택 프레임은 캐시에 항상 있는 단순한 bump pointer이고 오버플로에는 guard page 하나만 필요함
      제안한 방식은 guard page가 두 개 필요하고, 스택 조작도 두 배가 되며, 캐시 미스 가능성도 두 배가 됨
    • 글에서 잠깐 언급된 CPU 기능인 섀도 스택이 이와 꽤 비슷함. 다만 주된 목적은 보안 쪽임
    • 스택이 왜 “잘못된 방향”으로 자라서 잘못 동작하는 프로그램이 보안 문제를 일으키는지도 의문임
      이유는 알지만, 많은 것처럼 30년 전쯤에나 마지막으로 말이 됐고 그 영향은 흥미로웠음
    • Forth를 좋아할 준비가 된 것 같음 ;-)
      이상하게도 Wikipedia 글은 Forth가 매개변수 스택과 반환 스택 모두에 접근할 수 있다는 점을 모델의 큰 특징으로 잘 내세우지 않는 듯함
      https://en.wikipedia.org/wiki/Forth_(programming_language)
    • 반환 주소를 다른 곳에 저장하는 이점은 하드웨어가 섀도 스택 같은 걸 지원하지 않는 한 명확하지 않음
      별도 페이지로 옮기고 포인터 두 개를 관리하는 비용이, 이미 필요한 곳에만 보호를 제공하는 스택 쿠키/보호기보다 실질적으로 더 싸다는 걸 보여야 함
      지금의 스택 보호기보다 실질적인 보안 이점도 없음. 임의 읽기/쓰기가 가능하면 결국 제어 흐름 무결성 우회로 이어질 수 있음
  • Virgil은 프레임 포인터를 쓰지 않음. 동적 스택 할당이 없으면 특정 함수의 프레임 크기는 고정되어 있고, 단순한 이진 검색 테이블 조회로 찾을 수 있음
    Virgil의 기법은 페이지 인덱스 기반 범위를 추가로 사용해 평균적으로 비교 몇 번만으로 조회를 좁히며, 풀기 정보와 GC용 stackmap을 결합하고 공간도 매우 적게 씀
    주요 코드는 https://github.com/titzer/virgil/blob/master/rt/native/Nativ...에 있고, 같은 디렉터리의 나머지 코드는 메타데이터 디코딩을 구현함
    프레임 크기가 동적인 경우, 즉 스택에 데이터를 할당하는 경우에만 프레임 포인터가 의미 있다고 봄
    정적 메커니즘이면 충분한데 동적 메커니즘을 쓰는 건 이상하며, 주로 메타데이터 인코딩 ABI나 풀기 루틴에 합의가 없어서 그렇게 된 듯함
    1~2% 측정치는 믿을 만하고, 배열 경계 검사 비용과 비슷한 수준임
    디버깅과 프로파일링에는 1% 비용을 특별 대우하면서 보안 계층 추가에는 거부감을 보이는 건 우선순위가 매우 이상함

    • C에도 경계 검사를 추가할 수 있지만 1~2%보다 훨씬 큰 비용이 듦
      C++의 std::vector는 기본으로 경계 검사를 끄는데, C++가 완전히 미친 사람들에 의해, 또 그런 사람들을 위해 설계됐기 때문이라고 봄
      그 외에는 경계 검사가 없는 언어가 바로 떠오르지 않음
  • 좋은 글임. 프레임 포인터가 사라졌을 때는 아쉬웠음
    다른 시스템은 물론 Linux에서도 많은 사람이 프레임 포인터 부재 때문에 오랫동안 고통받았고, 가능한 많은 환경에서 유지하려 애썼음
    주류 Linux가 다시 가져오는 걸 보는 건 인정받는 느낌이지만 동시에 좀 답답하기도 함

    • 진심으로 궁금함. DWARF로 스택을 푸는 게 성가신 건 알지만, 시스템 전체 코드를 느리게 만들 만큼 그렇게 나쁜 이유가 뭔지 모르겠음
      Debian 계열에서 느린 건 라이선스 이유로 perf에 느린 풀기 경로만 패키징하기 때문인데, 도구가 괜찮으면 차이를 거의 못 느끼겠음
      뭘 놓치고 있는지 궁금함
  • 전반적으로 프레임 포인터에 찬성하지만, 이 영역에서 몇 년 일하며 느낀 점이 있음
    많은 프레임 포인터 기반 스택 풀기는 DWARF 풀기 정보에는 없는 문제를 고려하지 않음. 프레임 설정은 원자적이지 않고 push $rbpmov $rsp $rbp 두 명령으로 이뤄지며, 스냅샷이 push 중에 찍히면 부모 프레임을 놓칠 수 있음
    코드를 검사해 완화할 수 있을지도 모르지만, 스택 프레임과 무관한 push %rbp도 있을 수 있어 휴리스틱에 가까워 보임
    Brendan이 언급한 BPF 기반의 빠른 커널 내 풀기 해법도 개발했음: https://web.archive.org/web/20231222054207/https://www.polar...
    이 방식은 DWARF CFI를 그대로 쓰지 않고 BPF에서 쓸 수 있는 임의 접근 형식으로 변환함
    현재는 프레임 포인터가 있는 JIT 섹션만 지원하지만, JVM 인터프리터 풀기를 네이티브 풀기와 맞물리게 구현하는 건 가능하다고 봄
    이상적으로는 프레임 포인터 활성화가 사례별로 이뤄져야 하고, 벤치마킹이 핵심임
    산업과 소프트웨어 성격에 따라 성능, 관측 가능성, 비즈니스 지표 사이의 절충이 크게 달라질 수 있음
    Fedora 쪽은 여기서 매우 훌륭하고 엄밀하게 일했음
    또한 의존 라이브러리까지 포함해 시스템 전체에서 이 설정을 바꿀 수 있는 빌드 시스템은 테스트뿐 아니라 운영 적용에도 매우 유용함
    마지막으로 Indu가 작업 중인 SFrame이 기대됨. 사용자가 프레임 포인터 사용 여부를 선택하게 하면서 현재 문제를 많이 풀어줄 것 같지만, 인프라가 갖춰지고 모두가 업그레이드하기까지는 몇 년 걸릴 수 있음

    • 좋은 플레임 그래프를 얻으려면 Linux 배포판 전체에 걸쳐 프레임 포인터를 켜야 함
      실제로 무슨 일이 일어나는지 이해하려면 전체 시스템 분석이 필요하고, Fedora나 Debian 같은 현재 바이너리 Linux 배포판 구조에서는 다른 대안이 사실상 불가능함
    • 한 명령으로도 가능함: ENTER N,0은 지역 변수를 위해 N만큼 스택 공간을 예약하며 대략 PUSH EBP, MOV ESP,ESP, SUB SP,N과 같음
      다만 ENTER가 x86-64에 있는지는 기억나지 않음
      그래도 CALL과 프레임 설정 사이에서는 원자적이지 않아서, CALL 이후 ENTER 이전에 스냅샷이 찍히면 프레임 설정을 얻지 못함
      ENTER가 잘 쓰이지 않는 이유는 너무 느리다고 판단됐기 때문임
      LEAVE는 대체하는 명령열만큼 빠르거나 더 빠르기 때문에 쓰이지만, ENTER는 두 번째 피연산자가 성능을 망침
      그 피연산자는 중첩 함수가 상위 스택 프레임에 접근하기 위한 것이고 사용 비용이 매우 큼
    • 비원자적인 프레임 설정은 CPU 프로파일러에는 확실히 문제지만, 할당 프로파일링, Off-CPU 프로파일링, 인터럽트 기반이 아닌 다른 프로파일링에는 문제가 되지 않음
    • JVM 풀기 쪽도 좋은 진척이 있음
  • 프로파일에서 보이던 [unknown] 산이 왜 생기는지 이제야 알게 돼 흥미로움
    다만 정당화하기는 쉽지 않음. 2% 성능 차이는 실제로 꽤 큰 차이임
    프레임 포인터 포함 여부를 더 세밀하게 제어할 수 있으면 좋겠음
    세밀한 프로파일링이 있다면 특정 함수나 컴파일 단위에 프레임 포인터가 필요한지 판단할 수 있을 것임
    프레임 포인터 포함으로 극적으로 느려지는 작업은 소수이고 나머지는 별 영향을 받지 않는다는 결과가 나와도 놀랍지 않음

    • 2% 성능 차이는 그렇게 크지 않음. 특히 프로파일링으로 병목을 찾아 10% 이상의 개선을 얻을 수 있다면 더더욱 그렇다
    • GCC에서는 함수 선언에 속성을 붙여 함수별로 켜고 끌 수 있음. 다만 LLVM에서는 동작하지 않음
      __attribute__((optimize("no-omit-frame-pointer")))
      __attribute__((optimize("omit-frame-pointer")))
    • 실제 애플리케이션에서는 성능 비용이 2%보다 훨씬 작을 수 있음
      이런 벤치마크는 다소 인공적이라 완전히 믿으면 안 되고, 현실 애플리케이션은 결과가 크게 다를 때가 많음
      프로파일링은 중요하며, 코드를 세심하게 프로파일링해서 여러 구간을 최대 20%까지 빠르게 만든 적이 있음
      성능 손실에 아주 민감한 애플리케이션이라면 실험실에서는 프레임 포인터를 켜고 프로파일링한 뒤, 고객에게 배포하는 버전에서는 생략하면 됨
    • 측정된 오버헤드는 1%보다 약간 낮음. 과거에 프레임 포인터 때문에 성능이 크게 나빠진 드문 사례가 있었지만 지금은 수정됨
    • 보통은 2%보다 훨씬 작음
  • JIT 코드 지원은 아쉽게도 좋지 않지만, LLVM에는 생성된 각 메서드와 주소를 기록하는 훌륭한 훅이 있음
    그래서 단순한 혼합 모드 스택 풀기를 비교적 쉽게 만들 수 있지만, 주로 프로세스 내부에서 가능함
    Intel의 DNN 계열은 perf가 읽을 수 있는 공용 파일로 정보를 덤프하는 것 같지만, oneDNN의 커널 자체가 RBP를 계속 재사용해서 사실상 쓸모가 없음
    글의 “Java JVM 같은 JIT 런타임에는 DWARF 정보가 없다”는 주장도 놀라움
    기본으로 꺼져 있는 건지, 아니면 말 그대로 사용할 수 없는 건지 궁금함
    검색해 보면 대개 JVM 스택에 JNI/C 쪽을 포함하고 싶어 하는 내용으로 이어짐: https://github.com/async-profiler/async-profiler/issues/215