3P by GN⁺ | ★ favorite | 댓글 1개
  • CPython의 새 꼬리 호출 인터프리터는 처음엔 pyperformance 평균 10~15% 향상처럼 보였지만, 기준선을 조정하면 실제 개선 폭은 설정에 따라 약 1~5%로 좁아짐
  • 큰 폭의 개선은 새 구현만의 효과라기보다 LLVM 19 회귀를 우회한 결과에 가까웠으며, Clang 19가 기존 computed goto 디스패치를 제대로 복제하지 못한 것이 핵심 변수였음
  • Intel Raptor Lake i5-13500에서는 clang19 빌드가 clang18보다 1.09배 느렸고 clang19.tc는 1.03배 빨랐지만, Apple M1 MacBook Air에서는 clang19가 1.12배 느리고 clang19.tc도 1.00배 느린 수준에 머묾
  • LLVM의 tail duplication 제한으로 간접 점프 수가 clang18의 332개에서 clang19의 3개로 줄면서, computed goto 기반 인터프리터가 의도한 구조가 사실상 사라짐
  • 꼬리 호출 방식은 여전히 의미 있는 개선이지만, musttail처럼 컴파일러 최적화 여부를 명시적으로 요구하는 접근이 성능 민감 코드에는 더 견고할 수 있음

성능 개선처럼 보인 기준선 효과

  • CPython 프로젝트는 약 한 달 전 바이트코드 인터프리터의 새 구현 전략을 병합함
  • 초기 결과는 다양한 플랫폼과 pyperformance 벤치마크에서 평균 10~15% 성능 향상을 보였음
  • 이후 분석에서는 이 큰 향상이 주로 LLVM 19 회귀를 우연히 우회한 결과로 드러남
    • GCC, clang-18, 또는 특정 튜닝 플래그를 적용한 LLVM 19와 비교하면 향상 폭은 약 1~5% 수준으로 낮아짐
  • 꼬리 호출 인터프리터 자체는 실제 속도 향상을 만들지만, 초기 수치가 암시한 것보다 개선 폭은 완만함
  • clang-19 또는 그 이후 버전으로 빌드했다면 기존 경로가 실제로 10~15% 느렸을 가능성이 있음
    • Simon Willison은 python-build-standalone 빌드와 Python 3.13 비교에서 10% 속도 향상을 재현함

벤치마크 구성과 핵심 수치

  • 여러 CPython 빌드를 Intel 서버와 Apple M1 MacBook Air에서 비교함
    • Intel 서버는 Hetzner에서 운영하는 Raptor Lake i5-13500
    • 모든 빌드는 LTOPGO를 사용함
    • 빌드 재현에는 nix 구성을 사용함
  • 비교 대상은 다음과 같음
    • clang18: Clang 18.1.8, computed goto
    • gcc: GCC 14.2.1, computed goto, Intel만 해당
    • clang19: Clang 19.1.7, computed goto
    • clang19.tc: Clang 19.1.7, 새 꼬리 호출 인터프리터
    • clang19.taildup: Clang 19.1.7, computed goto와 회귀 우회용 -mllvm 튜닝 플래그
  • clang18을 기준선으로 둔 pyperformance 평균 결과는 다음과 같음
    • Raptor Lake i5-13500:
      • clang19: 1.09배 느림
      • clang19.taildup: 1.01배 빠름
      • clang19.tc: 1.03배 빠름
      • gcc: 1.02배 빠름
    • Apple M1 MacBook Air:
      • clang19: 1.12배 느림
      • clang19.taildup: 1.02배 느림
      • clang19.tc: 1.00배 느림
  • 꼬리 호출 인터프리터는 clang-18 대비 일부 속도 향상을 보였지만, clang-19로 이동하며 생긴 성능 하락 폭보다 작았음
  • clang18.tc는 측정하지 못함
    • 꼬리 호출 인터프리터가 Clang 19에 새로 들어간 컴파일러 기능에 의존하기 때문임
    • 이 제약 때문에 상황을 이해하려면 더 많은 벤치마크 조합이 필요했음

LLVM 19 회귀가 만든 디스패치 붕괴

  • 전통적인 바이트코드 인터프리터는 while 루프 안의 switch 문으로 opcode를 처리함
    • 컴파일러는 보통 switch점프 테이블과 간접 점프로 컴파일함
  • 오래전부터 각 opcode 본문에 디스패치 로직을 복제하면 이런 인터프리터를 빠르게 만들 수 있음
    • opcode 끝에서 루프 처음으로 돌아가는 대신, 다음 명령을 디코딩하고 점프 테이블을 인덱싱하는 로직을 opcode마다 별도로 둠
  • C 컴파일러는 label 주소를 가져와 computed goto로 사용하는 기능을 제공하며, CPython도 꼬리 호출 작업 전에는 이 방식의 인터프리터 루프를 사용함
  • Clang/LLVM은 컴파일러 성능상의 이유로 computed goto의 여러 goto를 내부적으로 하나의 indirectbr LLVM instruction으로 병합함
    • 이후 코드 생성 단계에서 tail duplication을 수행해 분기 로직을 각 위치에 다시 복제함
    • 이 흐름은 LLVM의 오래된 블로그 글에 고수준으로 문서화되어 있음
  • LLVM 19는 특정 경우의 심각한 컴파일 시간 또는 메모리 사용 증가를 피하려고 tail duplication pass 제한을 도입함
    • CPython에서는 이 제한 때문에 Clang이 디스패치 점프를 병합된 상태로 남겨둠
    • 그 결과 computed goto 기반 구현의 목적이 사실상 무력화됨
  • 이 문제는 비슷한 인터프리터 루프를 가진 다른 언어 구현에서 먼저 식별됐지만, CPython에 영향을 준다는 사실은 알려져 있지 않았음
  • 객체 코드를 디스어셈블해 간접 점프 수를 세면 차이가 직접 보임
    • clang18 빌드의 _PyEval_EvalFrameDefault: jmp * 332개
    • clang19 빌드의 _PyEval_EvalFrameDefault: jmp * 3개

computed goto가 보인 애매한 위치

  • tail duplication 로직 변화가 회귀를 일으킨 점은 수정 후 clang-18 수준 성능이 회복된다는 점으로 확인됨
  • 다만 회귀의 크기는 완전히 설명되지 않음
    • 역사적으로 opcode 디스패치 복제는 인터프리터를 20%에서 100%까지 빠르게 한다고 인용된 적이 있음
    • 현대 프로세서의 향상된 분기 예측기에서는 더 최근 연구가 약 2~4% 수준의 더 작은 향상을 보임
  • Python은 설정 옵션으로 단일 switch 문을 쓰는 구식 인터프리터도 지원함
    • clang18.nocg: clang18 대비 1.01배 빠름
    • clang19.nocg: clang18 대비 1.02배 느림
    • clang19: clang18 대비 1.09배 느림
  • clang19.nocgclang19보다 빠르게 나온 점은 추가적인 반전임
    • Clang 18 또는 적절한 플래그를 적용한 Clang 19는 switch 기반 인터프리터에서도 디스패치 로직을 각 opcode 본문에 복제함
  • 간접 점프 수 비교에서도 이 차이가 드러남
    • clang18: 332개
    • clang18.nocg: 306개
    • clang19.nocg: 3개
    • clang19: 3개
  • 현대 Clang에서는 computed goto 인터프리터 전체가 불필요한 복잡성일 수 있음
    • 컴파일러가 switch 기반 코드에서도 같은 변환을 수행할 수 있기 때문임
    • 반대로 computed goto 자체도 변환을 보장하기에는 충분하지 않았음
  • GCC 14.2.1은 switch를 복제하지 않았지만, computed goto를 사용할 때는 의도한 동작을 구현함

수정과 우회 방법

  • LLVM pull request 114990이 글 게시 직후 병합되어 회귀를 수정함
  • 병합 전 벤치마크에서도 해당 수정이 기대한 성능을 회복하는 것이 확인됨
  • 수정 전 릴리스에서는 회귀를 일으킨 PR이 추가한 튜닝 옵션으로 tail duplication 중단 임계값을 조정할 수 있음
    • clang-19에서 해당 제한을 매우 큰 값으로 설정하면 비슷한 동작을 회복할 수 있음
  • LTO 빌드에서는 이 옵션 전달이 복잡함
    • tail duplication은 코드 생성 중 일어나며, LTO 빌드의 코드 생성은 컴파일 시간이 아니라 링크 시간에 일어남
    • 따라서 플래그를 컴파일러뿐 아니라 lld에도 전달해야 함
  • 사용한 설정 예시는 ./configure 단계에서 OPTLDFLAGS-mllvm -tail-dup-pred-size=5000을 전달하는 방식임

벤치마킹이 드러낸 기준선 문제

  • 벤치마크는 특정 빌드 간 성능 차이를 정확히 측정할 수 있지만, 그 결과를 “일반적인 성능 향상”으로 확장하려면 추가 가정이 필요함
  • 꼬리 호출 인터프리터 벤치마크는 기존 computed goto 인터프리터 대비 10~15% 빠른 결과를 보였지만, 더 넓은 결론으로 일반화하기에는 기준선이 복잡했음
  • 성능 작업에서는 무엇을 기준선으로 비교하는지가 반복적으로 어려운 문제임
    • 현재 최고로 알려진 접근을 이론적으로 이해해도 실제 운영체제, 컴파일러 옵션, 플래그를 제대로 조정하는 것은 별도 문제임
    • 오래된 하드웨어나 재현하기 어려운 규모에서 나온 공개 벤치마크는 직접 비교에 적합하지 않을 수 있음
  • 머신러닝 논문에서도 알고리듬 개선을 주장할 때 “무엇을 했는가”보다 “어떤 기준선과 비교했는가”가 먼저 중요한 질문이 되는 경우가 많음
  • 잘못 튜닝된 기준선과 비교하면 인상적인 결과가 쉽게 나올 수 있음

최적화 컴파일러와 musttail

  • computed goto 사례는 최적화 컴파일러에 대한 기대가 서로 충돌할 수 있음을 보여줌
    • 컴파일러는 프로그래머의 의도를 존중하고 같은 동작을 보존해야 함
    • 동시에 코드를 빠르게 만들기 위해 복잡하고 직관적이지 않은 변환도 수행해야 함
  • clang-19는 computed goto 인터프리터를 프로그램 동작 관점에서는 올바르게 컴파일했지만, 최적화 의도와는 완전히 다른 출력을 만들었음
  • 다른 Clang 버전은 단순한 switch() 기반 인터프리터에도 의도했던 것과 같은 최적화를 적용함
  • 소스 코드 수준의 computed goto와 머신 코드 수준의 디스패치 복제는 거의 직교하는 개념처럼 나타남
    • 실행 결과가 같기 때문에 현재 도구는 이 차이를 일관되게 표현하기 어려움
  • 꼬리 호출 인터프리터는 musttail attribute에 기반함
    • musttail은 전통적인 관찰 가능한 프로그램 동작을 바꾸지는 않지만, 최적화기와의 대화에 가까움
    • 컴파일러가 특정 최적화를 수행할 수 있어야 하며, 해당 최적화가 일어나지 않으면 컴파일 실패를 요구함
  • 이런 방식은 컴파일러가 진화하는 동안에도 성능 민감 코드를 더 견고하게 작성하는 스타일이 될 수 있음
  • 가상의 [[clang::musttailduplicate]] 같은 속성으로 인터프리터 while 루프의 computed goto를 대체할 수 있을지도 검토할 만함

nix가 제공한 재현성과 한계

  • nix는 여러 Python 인터프리터 빌드를 관리하는 데 큰 도움이 됨
    • 실험 과정에서 gcc, clang-18, clang-19, clang-20 네 컴파일러와 다양한 플래그 조합으로 수십 개의 Python 인터프리터를 빌드하고 벤치마크함
  • nix를 사용해 병렬 버전을 재현 가능하고 격리된 방식으로 유지할 수 있었음
    • 특정 빌드가 어떤 컴파일러와 플래그에서 나왔는지 확신할 수 있었음
    • 빌드 매트릭스 정의도 짧은 추상화로 관리할 수 있었음
  • 버그 수정 패치를 적용한 커스텀 LLVM을 빌드하고, 그 컴파일러로 Python 빌드를 수행하는 것도 약 10줄의 코드로 가능했음
  • 단점도 남아 있음
    • nix는 일반적인 소프트웨어 사용 방식과 다른 부분이 있어, 이런 차이가 벤치마크나 결론에 영향을 줬을 가능성을 완전히 배제하기 어려움
    • 예를 들어 nix는 기본적으로 특정 hardening 플래그로 프로젝트를 빌드하며, 이 플래그가 꼬리 호출 인터프리터에 불균형하게 영향을 준다는 점을 초기에 발견함
  • Nix는 확장성과 커스터마이징이 강하지만, 특정 커스터마이징 방법을 찾으려면 많은 시행착오와 nixpkgs 소스 탐색이 필요했음

댓글과 토론

Hacker News 의견들
  • CPython에 꼬리 호출 인터프리터를 넣은 PR 작성자임
    먼저 이 문제의 원인을 찾는 데 거의 한 달을 쓴 Nelson에게 감사함
    기준선에 사용한 컴파일러에 그런 버그가 있을 거라고 저도, 아마 CPython 팀도 예상하지 못했고, 큰 실수를 해서 매우 부끄럽고 미안함
    사과 글도 올렸음: https://fidget-spinner.github.io/posts/apology-tail-call.htm...

    • “큰 실수라서 매우 부끄럽고 미안하다”는 말을 보고 CPython 성능을 망가뜨린 줄 알았는데, 실제로는 그런 상황이 전혀 아님
      10~15% 성능 향상이라고 발표했지만 버그 없는 컴파일러에서는 1~5% 에 가깝다는 얘기이고, 그 숫자도 완전히 틀린 게 아니라 특정 조건에서만 맞았던 것뿐임
      개선을 만들고, 측정했고, PR도 리뷰받았으니 할 일은 한 셈임. 마침 측정에 쓴 clang 버전 문제 때문에 수치가 오해를 낳은 건데, 누구나 빠질 수 있는 합리적인 실수로 보임
      그래도 의미 있는 성능 개선을 가져왔고 컴파일러 회귀도 찾아냈으니, 잘못된 숫자는 그에 비하면 작아 보임. 이 일로 실제로 누가 피해를 봤는지도 잘 모르겠고, 사과까지 할 일은 아닌 듯함
    • 참고로 그 블로그 글을 쓴 뒤에 수정 사항은 병합됐음 ;)
      Python 인터프리터처럼 오래된 시스템에서 3~5%가 유지된다면 그 자체로도 큰 성과라서 충분히 자랑스러워해도 됨
      30년쯤 지나 보니 오래 존재한 시스템에서 의미 있는 성능 향상, 특히 1%를 넘는 향상은 일단 의심하게 됨
      실제 향상도 있긴 하지만 흔하지 않고, 종종 시간을 다른 곳으로 옮겼을 뿐이라 벤치마크에 잡히지 않는 경우가 있음. 또 벤치마크는 효과를 분리하려고 통제된 환경에서 하는데, 실제 소프트웨어는 VM이나 데스크톱에서 온갖 다른 것들과 같이 돌아감
      분리된 환경에서 확실히 커 보이던 개선이 운영 환경에 들어가면 사라지거나 음수가 되는 경우를 많이 봤음
      CPython은 여러 환경을 대상으로 해야 해서 더 어렵고, “운영에서 빨라지지 않으면 진짜 빨라진 게 아니다”라고 할 단일 운영 대상도 없음. 그런 세계에서 성능을 개선하려는 건 정말 힘듦
      결국 성능 튜닝과 측정은 매우 어렵고, 미안해할 일은 그 사실을 배운 것 정도임
      틀리는 걸 두려워하지 않았으면 함. 어차피 누구나 틀림. 지금처럼 “이건 우리가 망친 것 같다”고 말하고, 어떻게 처리할지와 앞으로 피할 방법을 찾으면 됨
      [1] 성능뿐 아니라 사람의 프로세스에서도 흔함. 예를 들어 코드 리뷰 도구 팀이 “코드 리뷰 시간을 15% 줄여 모두의 작업 흐름을 빠르게 했다”고 해도, 실제로는 시스템의 다른 부분에 일을 더 만들었고 전체 흐름은 빨라지지 않았으며 측정하지 않은 곳으로 15%를 옮겼을 수 있음
    • 꼬리 호출 인터프리터 설계의 주된 동기 중 하나는 최적화기의 변덕에 덜 취약해지는 것이라고 봄. 이 기법을 다룬 원래 글(https://blog.reverberate.org/2021/04/21/musttail-efficient-i...)에서도 그렇게 설명했음
      이론적으로는 이런 제어 흐름 그래프와 프로파일이 있으면 컴파일러가 전통적인 switch() 기반 인터프리터에 최적 코드를 만들 정보가 충분해야 함. 하지만 실제로 함수가 이렇게 크고 연결되어 있으면 컴파일러와 싸우게 됨
      레지스터에 유지하길 원하는 중요한 변수를 스필하고, fallback 함수 호출 주변으로 줄여 넣고 싶은 스택 프레임 조작을 끌어올리고, 분기 예측 때문에 분리하고 싶었던 동일 코드 경로를 합쳐버림. 장갑을 낀 채 피아노를 치는 느낌이 될 수 있음
      여기서도 바로 그 “동일 코드 경로 병합”이 일어났고, “버그 있는” 컴파일러가 동일 경로를 합쳐 성능을 나쁘게 만들었음
      “수정된” 컴파일러는 더 이상 그러지 않지만, 그 수정도 결국 컴파일러 내부 휴리스틱을 조정한 것에 가까움. 이 컴파일러나 다른 컴파일러가 앞으로도 우리에게 유리한 방식으로 휴리스틱을 유지한다는 보장은 없음
      반면 꼬리 호출 인터프리터는 원하는 기계어 패턴을 인터프리터 자체에 표현할 수 있음. musttail, noinline, preserve_none 속성을 함께 쓰면 최적화기 휴리스틱에 훨씬 덜 휘둘리도록 문제를 제약할 수 있음
      그래서 꼬리 호출 인터프리터의 이점은 단순한 3~5% 성능 향상 이상이고, 일부 컴파일러에서는 그보다 더 큰 신뢰 가능한 성능 향상일 수 있음
    • “미안하다, 내가 실수했다”고 말할 수 있는 태도에 존중을 보냄. 요즘 표준처럼 보이는 가짜로 버티다 성공한 척하기 문화가 정말 싫음
    • 기준선 성능 회귀가 faster-cpython 벤치마크 페이지 [0]에는 왜 나타나지 않았는지, 혹은 나타났는지 궁금함
      비슷한 일을 막도록 벤치마크를 개선할 수 있을까?
      [0] https://github.com/faster-cpython/benchmarking-public
  • 벤치마킹은 제대로 하기가 정말 미치도록 어려움. 사람을 속이는 요소가 너무 많음
    최근 어떤 알고리즘을 약 15% 빠르게 만드는 방법을 찾았다고 생각했음. 적어도 모든 벤치마크가 그렇게 말했음
    그런데 테스트 하네스에 더 빠른 함수를 복제해 넣고 실제로는 호출하지 않은 채 원래의 느린 버전만 호출했는데도 여전히 15% 빨랐음. 실행되지도 않은 코드가 원래 코드를 빠르게 만든 셈임
    당연히 코드와 메모리 배치 문제였고, 무언가가 이동하면서 CPU 캐시에 더 잘 맞아떨어진 것임
    얻은 속도 향상이 코드가 실제로 “더 좋아져서”인지, 아니면 어디선가 더 나은 정렬을 운 좋게 얻어서인지 알기가 정말 어렵다
    Casey Muratori가 Substack에서 이런 주제로 아주 흥미로운 시리즈를 쓰고 있음

    • 그런 링커 복권이 15% 향상까지 만들었다니 놀랍다. 어떤 경우에 그런 큰 향상이 나오는지, 드문 일인지, 결국 어떻게 판단했는지 궁금함
    • 컴파일러 결정을 일부러 무작위화해서, 코드가 실제로 얼마나 잘 수행되는지 더 안정적으로 추정하고 링커 복권에서 이기거나 진 결과에 덜 휘둘리게 하던 벤치마킹 프로젝트가 있었던 걸 어렴풋이 기억함
    • 오랫동안 Java “성능 엔지니어”로 활동한 Aleksey Shipilёv가 벤치마킹의 어려움에 대해 글과 발표를 많이 남겼음. 그의 블로그 글이나 발표를 강력히 추천함
  • 실제 사정을 파고들어 밝혀낸 글 작성자에게 찬사를 보냄. Python 3.14의 꼬리 호출 인터프리터는 여전히 좋은 개선이고, 언어 런타임에서 몇 퍼센트 향상은 어렵게 얻어지는 성과임
    다만 마법 같은 15% 공짜 점심은 아니었음
    더 중요한 건 이번 일이 벤치마킹 엄밀성과 여러 환경에서 테스트하는 것의 중요성을 잘 보여줬다는 점임. 모두에게 이득이 될 수 있는 컴파일러 버그도 드러냈음
    다음번 큰 성능 향상 주장을 다시 확인하게 만드는 종류의 심층 분석임. 더 생각해볼 질문은, 지금 나와 있는 수많은 “X% 더 빠름” 결과 중 얼마나 많은 것이 실제로는 벤치마크 인공물이나 알려지지 않은 회귀 때문이냐는 것임
    앞으로 이런 함정을 더 잘 피하려면 어떻게 해야 할까?

    • 더 큰 의문은 결함 있는 컴파일러 기능이 들어갔을 때 Python 성능 10% 하락이 왜 감지되지 않았느냐임
      컴파일러 자체는 벤치마크하지 않는 건가? 컴파일러 쪽이나 Python 쪽 기존 벤치마크가 해당 컴파일러를 쓰지 않았던 건가?
  • 이건 C가 “기계에 가깝다”거나 “이식 가능한 어셈블리”라는 말이 얼마나 맞지 않는지 보여주는 좋은 예임. 현대 최적화기는 관찰 가능한 효과가 없으면 논리를 과감하게 바꿔버림
    글에서도 “clang-19는 computed-goto 인터프리터를 결과 바이너리가 기대한 값을 모두 낸다는 의미에서는 ‘올바르게’ 컴파일하지만, 동시에 그 출력은 최적화 의도와 완전히 어긋난다. 게다가 다른 컴파일러 버전들은 ‘순진한’ switch() 기반 인터프리터에 최적화를 적용해, 우리가 소스 코드를 다시 써서 ‘의도했던’ 것과 정확히 같은 최적화를 구현한다”고 했음

    • 다른 80~90년대 시스템 프로그래밍 언어 관점에서는 C가 여전히 꽤 이식 가능한 어셈블리에 가까움
      C의 a += 1은 숫자 값을 증가시킨다고 믿을 수 있지만, C++의 같은 표현은 메모리를 할당하거나 호출 스택을 풀거나 알 수 없는 일을 할 수 있음. 마찬가지로 a = "a"는 C에서는 단순 포인터 대입이지만 C++에서는 메모리 할당 등이 일어날 수 있음
      “C는 이식 가능한 어셈블리”라는 표현은 각 문장이 동등한 기계어로 직접 컴파일된다는 뜻이 아님
    • “관찰 가능한 효과 없음”이 1만 단어짜리 블로그 글까지 늘어난 셈임
  • 컴파일러가 반복문 구성을 만지면서 꼬리 호출 인터프리터 전체가 발표만큼 효과적이지 않게 된 것이라 놀랍지 않음

    1. CPU 아키텍처와 버전이 매우 중요함. 문제의 95%는 분기 예측기가 최적으로 작동하도록 명령 디스패치 코드를 배치하는 일인데, C는 원래 이런 걸 지원하려고 만든 언어가 아님
    2. C 추상 기계도 의도를 제대로 표현할 만큼 저수준이 아님. 어떤 구현이든 특정 컴파일러와 특정 버전의 특성에 지나치게 민감해짐
      편집증적인 인터프리터 구현은 다시 어셈블리를 직접 쓰기도 함. LuaJIT는 매우 효율적인 어셈블리 루프 구현을 아키텍처 간에 이식 가능하게 만들려고 매크로 시스템을 구현한 것으로 유명함. 그래서 이런 걸 만져보는 게 재미있기도 함
      몇 년 전 인기 있는 인터프리터 루프 구현 방식에 대한 글과 테스트도 만들어뒀음:
      https://github.com/vkazanov/bytecode-interpreters-post
    • 작성자로서, “문제의 95%가 분기 예측기가 최적으로 작동하도록 명령 디스패치 코드를 배치하는 일”이라는 말은 이제 더 이상 참이 아니라는 걸 이 글을 쓰며 배웠음
      최신 분기 예측기는 실행 구간이 충분히 길고 해석되는 코드 자체의 동작이 안정적이면, 단일 간접 점프도 거의 정확히 예측할 수 있음
      이를 실제 하드웨어와 특정 시뮬레이션 분기 예측기 양쪽에서 연구한 논문이 있음: https://inria.hal.science/hal-01100647/document
      이 프로젝트에서 해본 실험도 일화적으로는 같은 결론을 지지함. 글에는 못 넣었지만 하드웨어 CPU 카운터와 perf stat으로 몇 가지 인터프리터를 봤고, 분기 오예측이 지배적인 요인으로 나타나지 않았음
  • Python 빌드 성능을 평가하는 건 극도로 어려움. 성능을 개선할 수 있는 빌드 기법이 너무 많기 때문임
    최근 astral 쪽에서도 conda-forge 빌드가 대부분보다 눈에 띄게 빠르다는 걸 보이며 이런 문제를 겪었음:
    https://github.com/astral-sh/python-build-standalone/pull/54...
    기존의 다른 빌드 최적화와 함께 썼을 때 꼬리 호출 인터프리터가 어떻게 동작하는지 궁금함

  • 관련 논의:
    https://docs.python.org/3.14/whatsnew/3.14.html#whatsnew314-... --> https://news.ycombinator.com/item?id=42999672 (66 points | 25 days ago | 22 comments)
    https://blog.reverberate.org/2025/02/10/tail-call-updates.ht... --> https://news.ycombinator.com/item?id=43076088 (124 points | 18 days ago | 92 comments)

  • 좋은 글임. 한 가지 세부 내용이 눈에 띄었음
    참고된 글 중 하나인 https://simonwillison.net/2025/Feb/13/python-3140a5/에서 “3.14.0a5가 벤치마크에서 3.13보다 1.12배 빨랐다, 내 극도로 과부하된 M2 MacBook Pro에서”라고 썼음
    이 부분이 꽤 헷갈림. 다른 프로세스로 컴퓨터가 과부하된 상태에서 벤치마크를 돌렸다는 뜻인가? 그러면 결과가 완전히 신뢰할 수 없지 않나?
    이런 벤치마크는 외부 변수를 제거하려고 매우 통제된 환경에서 수행한다고 생각했음

  • 여기서 어떤 사람들은 10%를 “크다”, 1%를 “정상”이라고 하지만, 이중 재귀 Fibonacci의 부분 인라이닝 같은 최적화는 실제 작업량과 시간을 지수적으로 줄일 수 있음
    두 자리 인자에서는 10배 이상, 즉 수천 퍼센트도 가능함. 엄밀히는 문제 크기가 아니라 재귀 깊이 차이에 대해 지수적임 [1]
    C 컴파일러도 코드 인라이닝 지표에 매우 까다롭게 반응할 수 있어서, 그 엄청난 속도 향상이 실제로 나타나는지는 코드 형태에 매우 민감할 수 있음
    그래서 문제의 일부는 CPU가 매우 정교하고 복잡해졌다는 것이지만, 또 다른 측면은 -O0이나 -O1을 넘어선 컴파일러도 정교하고 복잡해졌다는 것임
    이 글은 좋고 읽을 가치가 있지만, 복잡한 두 가지가 상호작용하면 매우 놀라운 결과가 나올 수 있다는 수많은 예 중 하나이기도 함. 이는 컴퓨팅 바깥에서도 참임
    사람들은 이 교훈이 몇 번이나 반복돼도 지나치게 단순화하려는 경향이 강함
    추가로, 글에서는 적어도 Intel과 Apple M1 두 CPU, gcc와 clang 두 컴파일러를 쓰지만, 실제 배포 환경에는 훨씬 많은 Intel, AMD, ARM 세대와 구현, 그리고 다른 컴파일러가 있을 수 있음. 전체 복잡도 중 아주 작은 일부만 표본으로 삼은 셈임
    더 과학적으로 하려면, 특히 “1.01배” 같은 차이에서는 시간 측정에 어떤 형태로든 오차 막대가 있어야 함. 평균의 표준편차나, 이런 경우에는 최솟값의 표준편차가 더 나을 수도 있음 [2]
    측정 오차를 줄이려면 OS에서 CPU 코어 고정 스케줄링도 필요할 가능성이 큼
    [1] https://stackoverflow.com/questions/360748/computational-com...
    [2] https://github.com/c-blake/bu/blob/main/doc/tim.md

  • 최근 Python 3.9부터 3.13까지 벤치마크를 해봤는데, 3.11까지는 계속 좋아졌음
    그런데 Python 3.12와 3.13은 3.11보다 약 10% 느렸음
    직접 만든 벤치마크가 충분히 좋지 않다고 생각했지만, 그래도 핵심 서비스에 배포해봤고 수집한 지표에서도 같은 변화가 보였음
    같은 문제를 겪은 사람이 있나?