Hacker News 의견들
  • 비슷한 맥락으로, Wren 인터프리터 성능을 다룬 이 페이지가 꽤 흥미로웠음
    Zef 글이 구현 기법 중심이라면, Wren 쪽은 언어 설계 자체가 성능에 어떻게 기여하는지도 보여준다고 느꼈음
    특히 Wren은 dynamic object shapes를 포기해서 copy-down inheritance가 가능해지고 메서드 조회가 훨씬 단순해진 점이 좋아 보였음
    개인적으로는 꽤 괜찮은 트레이드오프라고 봄. 클래스가 만들어진 뒤에 메서드를 덧붙여야 할 일이 실제로 얼마나 자주 있나 싶은 생각임

    • 인터프리터나 JIT 속도는 언어 설계가 엄청 크게 좌우한다고 봄
      동적 언어용으로 매우 최적화된 VM은 많지만, LuaJIT가 강한 이유는 Lua가 워낙 작고 최적화에 잘 맞는 언어이기 때문이라고 느낌
      최적화하기 까다로운 기능도 조금 있긴 하지만, 수가 적어서 공을 들일 만하다는 점이 큼
      반면 Python은 전혀 다르다고 느낌. 과장 조금 보태면 빠른 JIT 가능성을 최소화하도록 설계된 수준이고, 여러 겹의 동적성이 겹쳐 최적화가 정말 어려워 보임
      그렇게 오랜 기간 작업한 뒤에도 CPython 3.15 JIT가 x86_64에서 기본 인터프리터보다 약 5% 빠른 정도라는 점이 이를 잘 보여준다고 느낌
    • 이런 접근은 monkey patching이 관용적으로 받아들여지는 언어들, 특히 Ruby에서 늘 하는 일과 비슷하다고 봄
      물론 Ruby는 속도 최우선 성향으로 알려진 언어는 아니라는 점도 같이 떠오름
      반대로, 어떤 타입이 적용 가능한 함수 집합을 닫힌 형태로 가진다는 발상은 조금 의문스럽게 느껴지기도 함
      세상에는 임의 함수를 정의해 두고 첫 번째 인자 타입이 맞는 변수에 점 표기 메서드처럼 붙여 쓸 수 있는 언어가 꽤 있음
      예를 들면 Nim의 매크로, Scala의 implicit classes와 type classes, Kotlin의 extension functions, Rust의 traits 같은 방식임
    • 제 경험상 대체로 어떤 표현식에 정적 타입을 붙일 수 있으면 꽤 효율적으로 컴파일할 수 있음
      복잡한 동적 언어들은 여러 방식으로 그 가능성을 적극적으로 깨뜨리기 때문에 최적화가 어려워지는 편이라고 느낌
      돌이켜보면 꽤 당연한 이야기처럼 보임
  • 변경 #5에서 #6으로 넘어가며 inline caches와 hidden-class object model이 대부분의 성능 향상을 만든다는 점이, 역사적으로 V8이나 JSC가 빨라진 방식과 정말 비슷하게 느껴졌음
    순진한 인터프리터가 죽는 지점은 결국 프로퍼티 접근의 동적 디스패치이고, 나머지는 비교적 rounding error에 가깝다는 인상임
    각 단계가 얼마나 기여했는지를 따로 볼 수 있게 정리된 점도 좋았음. 보통 성능 글은 최종 수치만 던지고 끝나는 경우가 많음

    • #6에서 특히 흥미로운 구현 디테일은, AST를 직접 걷는 인터프리터에서 inline caching을 어떻게 하느냐는 점이었음
      바이트코드 인터프리터에서는 바이트코드 스트림의 안정된 오프셋을 패치하면 되니 IC 재작성 위치가 자연스러움
      그런데 여기서는 캐시 위치가 AST 노드라서, @pizlonator가 constructCache<>를 통해 generic 노드 위에 specialized AST 노드를 in-place로 세우는 방식을 썼다는 점이 인상적이었음
      결국 AST 레벨의 self-modifying code처럼 보였음
      대신 이 방식은 mutable AST nodes를 요구해서, 서브트리 공유나 병렬 컴파일처럼 많은 컴파일러가 기대하는 immutable AST 가정과 충돌함
      단일 스레드 인터프리터라면 깔끔하지만, 같은 AST를 백그라운드 스레드에서 JIT 컴파일하는 동안 인터프리터가 노드를 바꾸는 상황이라면 문제가 될 것 같음
    • 전반적인 방향엔 동의하지만, 이건 어디까지나 특정 벤치마크 하나에 대한 결과라는 작은 단서는 있다고 봄
      제 생각엔 실제 현업 코드의 대부분을 잘 대변하진 않을 수도 있음
      그렇게 느낀 이유는 sqrt 최적화로 1.6% 개선이 나왔다는 대목 때문이었음
      그 정도 개선이 나오려면 애초에 벤치마크 시간이 1.6% 이상은 거기에 쓰이고 있어야 해서 꽤 놀라웠음
      git repo를 보니 실제로 nbody 시뮬레이션에서 그런 일이 벌어진 듯했음
  • 저도 최근에 제 AST-walking interpreter 첫 버전을 공개한 직후라서 더 흥미롭게 읽었음
    제 목표는 인터프리터 언어를 만드는 데 무엇이 필요한지 기초 수준에서 이해하는 것이었음
    최적화 복잡성은 넣고 싶지 않았고, 그냥 제 Rust 코드를 스스로 이해할 수 있게 만드는 데 집중했음
    그런데 좋아하는 언어인 Rust를 썼다는 이유만으로도 성능이 꽤 잘 나와서 놀랐음
    게다가 Rust가 ownership과 lifetimes를 챙겨주니 garbage collector가 따로 필요 없다는 점도 보너스처럼 느껴졌음
    물론 지금은 closure 같은 부분에서 lifetime 지옥을 피하려고 clone에 꽤 보수적으로 의존하고 있지만, 그래도 속도와 메모리 프로파일은 충분히 괜찮다고 느낌
    단순하고 이해하기 쉬운 Rust 기반 tree-walking interpreter에 관심 있다면 제 인터프리터 gluonscript를 보면 됨

  • 글이 정말 좋았음
    특히 Arguments 아크, 즉 #7에서 #13으로 가는 흐름이 제 경험과 아주 비슷하게 닿았음
    예전에 Rust로 async step evaluator를 만들면서, 평소엔 borrow가 이득을 줄 거라 믿고 Cow<'_, Input>에 깊게 들어간 적이 있었음
    마이크로벤치에서는 좋아 보였지만, 실제 워크로드에서는 Cow의 discriminant와 lifetime 관련 복잡성이 첫 await 이후 모든 combinator로 번졌고, 인라이닝이 크게 무너지면서 Cow를 쓴 이유 자체가 사라졌음
    결국 evaluator 경계에서 NoInput / OneInput / MultiInput(Vec)로 갈아탔는데, 보기엔 투박해도 결과적으로 여기의 ZeroArguments / OneArgument / TwoArguments 분리와 거의 같은 지점에 도달했음
    한 가지 계속 궁금한 점은, native 경로에서 arity specialization 위에 type specialization까지 쌓아봤는지 여부임
    예를 들어 binary 스타일로 가면 isInt 검사 자체를 없앨 수도 있을 것 같음
    제 추측으로는 코드 크기 계산이 안 맞았거나, 객체 쪽은 IC가 뜨거운 경로를 이미 충분히 먹어버려서 native fast path 영향이 크지 않았던 것 같기도 함
    어느 쪽인지 궁금했음

  • 이건 정말 흥미롭고 잘된 작업이라고 느낌
    저도 비슷한 일을 해봤는데, 좀 더 함수형에 가까운 언어인 Scheme 쪽이었음
    여기서는 객체 최적화가 가장 큰 이득을 줬지만, 제 경우엔 closures 최적화가 제일 큰 승부처였음
    재미있게도 최적화 방식 자체는 꽤 비슷했음
    Scheme를 충분히 빠르게 만드는 답은 Three implementation models for scheme에 거의 다 들어 있다고 봄
    다만 이쪽은 어느 정도 컴파일 단계를 거치기 때문에 원래 AST를 그대로 인터프리팅하는 모델은 아니라는 차이가 있음

  • 흥미로웠고 공유해줘서 고맙다는 느낌임
    저도 언젠가 이 주제를 자세히 파보고 싶다는 생각이 들었음
    그리고 Github 기준으로 repo가 99.7% HTML에 0.3% C++라는 점도 꽤 웃기고 인상적이었음
    인터프리터 크기가 정말 작다는 증거 같았음

    • 정적 생성한 사이트를 커밋해둬서 그렇게 보이는 것임
      브라우저용 코드를 생성하는 방식 때문에 사이트 쪽이 쓸데없이 커진 면이 있음
      그래도 인터프리터 자체는 정말 아주 작음
  • 이 작업을 하면서 fil c 자체를 더 좋게 만들 만한 걸 배웠는지 궁금했음

    • 확실히 unions를 다루는 방식은 더 나은 해법이 필요하다는 걸 느꼈음
      그리고 value object의 메서드를 outline call로 처리하는 비용이 꽤 크다는 점도 배움
  • Lua가 포함된 건 봤는데, LuaJIT도 같이 있었으면 좋겠다는 생각이 들었음

    • 제 예상으로는 LuaJIT가 Zef를 완전히 이길 것 같음
      아니, 그 정도 엔지니어링이 들어갔다는 점을 생각하면 오히려 그래야 한다는 기대임
      넣을 수 있었던 런타임은 많았지만 다 포함하진 않았음
      그리고 PUC Lua가 QuickJS나 Python보다 꽤 빠르다는 점도 상당히 인상적이었음
  • Fil-C를 실제로 써본 경험이 어떤지, 실전에서 실질적 유용성이 있는지 궁금했음

    • 제가 Fil 본인이니 편향은 있음을 먼저 말해둠
      그래도 이 프로젝트에서는 꽤 실질적으로 도움이 됐음
      메모리 안전성 문제를 여러 개 결정론적으로 잡아줘서, 객체 모델 설계가 그렇지 않았을 때보다 훨씬 쉬워졌음
      또 정확한 GC가 붙은 C++은 정말 좋은 프로그래밍 모델처럼 느껴졌음
      일반 C++ 대비 생산성이 1.5배쯤 올라가는 기분이었고, 다른 GC 언어와 비교해도 1.2배쯤은 빠르게 개발되는 느낌이었음
      이유는 C++의 API 생태계가 풍부하고 lambdas, templates, class system이 매우 성숙했기 때문이라고 봄
      물론 여러 면에서 편향이 있다는 점도 인정함
      Fil-C++를 직접 만들었고, C++도 한 35년쯤 써왔음
  • 글에서 언급된 YOLO-C/C++ 컴파일러가 뭔지 궁금했음
    검색해도 잘 안 나오고 chatgpt도 모르는 듯했음

    • Fil-C의 작성자이자 이 언어의 작성자가 Yolo-C/C++ 라는 표현으로 Fil-C가 없는 일반적인 C/C++을 뜻해 쓴 것임