There's no reason for software to be slow anymore

Dan Luu

이제 소프트웨어가 느릴 이유가 없다

원문은 Dan Luu님이 에 게재했습니다. 이 블로그 구독하기

며칠 전, LLM 때문에 코드가 느리고 비대해졌다고 말하는 사람들은 나중에 모든 걸 초최적화된 어셈블리로 다시 작성하면 결국 망신을 당하게 될 거라는 바이럴 트윗을 봤다. 아직 모든 걸 어셈블리로 작성하고 싶은 단계는 아니다만, Nolan Lawson이 테스트에 대해 한 말 — 이제 버그가 몇 개일지는 스스로 정할 수 있다 — 의 변주라고 할 수 있는, 내가 여기에서 좀 더 서툴게 언급한 바 있는 내용이 성능에 대해서도 점점 더 사실이 되어가고 있다.

지난 글의 한 댓글에 대한 답으로, 예전에는 전문성이 필요했던 성능 작업의 비용이 수 자릿수 규모로 떨어졌고 과거에는 희귀한 기술을 가진 사람이나 팀이 필요했던 성능 작업을 이제는 몇 문장만 입력할 수 있는 사람이라면 누구나 할 수 있게 됐다1는 말, 즉 과거에는 가장 큰 규모나 가장 수익성 높은 프로젝트가 아니면 시도할 가치가 없을 만큼 비쌌던 온갖 최적화를 이제는 할 수 있게 됐다는 말에 대해, Marc Brooker는 이렇게 답했다

마지막 부분에 완전히 동의합니다. 특정 워크로드 하나에 맞춰진 동적 맞춤형 소프트웨어 — 워크로드의 한 부류가 아니라 — 가 매우 가능성 높은 결과가 될 것 같습니다. (물론 그 자체로 재미있는 위험과 기회가 따릅니다.) FFTW가 떠오르네요. 그리고 아주 특정한 문제(그리고 종종 아주 특정한 하드웨어)에서 엄청나게 빠르고 작게 만드는 데 집중했던 수많은 기묘한 옛 데모씬 기법들도 그렇죠. 예를 들어, 캐시 지역성을 극대화하려고 코드를 텍스처로 재활용한 데모를 기억합니다.

그리고 Michael Malis는 이렇게 지적했다

AI가 도움이 되지 않는다는 밈이 돌았는데, “코드는 원래 어려운 부분이 아니었다”는 것이었다. 어떤 분야에서는 그게 맞다고 생각하지만, 다른 분야에서는 코드를 작성하는 것 자체가 분명 어려운 부분이었다. JIT 컴파일러가 좋은 예다. 많은 소프트웨어에서 JIT 컴파일러는 코드를 빠르게 하는 데 큰 도움이 된다. JIT 컴파일러가 드문 걸 보면, 과거에는 JIT 컴파일러를 구현하는 일이 너무 어려워 시도할 가치가 없었다고 믿게 된다. LLM은 진입 장벽을 낮추고 JIT 컴파일러를 훨씬 쉽게 작성할 수 있게 했다. 이것이 pgrust의 핵심 아이디어다. 데이터베이스는 역사적으로 구축하기 가장 어려운 소프트웨어였고 그래서 한계가 있었다. 이제 AI와 함께라면 우리가 만드는 소프트웨어의 종류에 대해 더 야심 차게 생각할 수 있다.

워크로드 클래스에 대한 최적화

FRE, 지난 글에서 만든 정규식 엔진으로 실험해 보자. 이 엔진은 rebar 정규식 벤치마크 스위트에 접근할 수 있는 에이전트에게 한 달 동안 정규식 엔진 성능 개선을 반복시킨 결과물임을 기억하자. 그 결과 FRE는 우리가 홀드아웃 벤치마크가 있다고 에이전트에게 경고하기 전까지 rebar에 심하게 과적합됐다. 그러자 에이전트는 최적화를 충분히 일반화해 홀드아웃에서도 성능이 그런대로 괜찮아졌다. 홀드아웃 벤치마크에서 잘 테스트된 정규식 엔진을 이기지 못하는 “소프트웨어 공장” 정규식 엔진을 쓸 특별한 이유는 없지만, FRE에서 주목할 만한 점 하나는 네이티브 AOT 컴파일 버전이 긴 검색에서 꽤 잘했다는 것이다. 우리는 네이티브 코드 컴파일러를 ripgrep이 일반 매처를 실행하는 동안 다른 스레드에서 돌리고, 컴파일이 끝나면 네이티브 코드로 전환하면 전반적으로 더 좋은 성능을 얻을 수 있으리라는 점을 언급한 바 있다. 물론 컴파일 때문에 스레드 하나를 잃게 되므로 짧은 쿼리에서는 오히려 성능이 나빠지는 경우가 많겠지만, 나는 ripgrep이 몇 초 동안 도는 경우보다 수십 초나 수 분 동안 도는 데 얼마나 걸리는지를 훨씬 더 중요하게 생각하므로 그 트레이드오프는 감수할 만하다.

몇 분 정도의 사람 시간으로 정규식 엔진을 만들 수 있었던 것처럼, 이 실험 역시 몇 분 정도의 사람 시간으로 시도할 수 있다. 나는 몇 문장을 입력했고, 에이전트가 가서 (사람이 직접 하려면 꽤 많은 코드 수술이 필요한) 이를 가능하게 하는 작업을 수행했으며, 내 codex 히스토리에서 나온 실제 ripgrep 쿼리로 벤치마크를 돌렸다. 더 긴 쿼리에서는 아주 단순한 몇몇 쿼리에서 2~4배의 성능 향상을 볼 수 있다. 하지만 대부분의 쿼리는 더 복잡하고, 대표적인 홀드아웃 쿼리로 실행해 보면 AOT를 활성화해야 하는 쿼리에서는2 약 7% 정도의 속도 향상을 얻는다. 세상을 뒤흔들 결과는 아니지만, codex에 몇 분 타이핑한 대가로 얻은 결과치고는 나쁘지 않다(게다가 아직 추가 최적화를 진행 중이니 앞으로 더 빨라질 것이다).

인덱스를 만들까?

이건 따지고 보면 좀 어리석은 짓일 수도 있다. 컴퓨터에서 텍스트를 반복적으로 검색한다면, 속도를 높이는 가장 당연한 방법은 정규식 매칭용 네이티브 코드 컴파일러를 작성하는 게 아니라 인덱스를 만드는 것이기 때문이다. 하지만 여기서 말하고 싶은 요점은 단지 예전에는 상당한 시간과 전문성이 필요했던 이런 종류의 기술적 작업이 이제는 아주 손쉽게 할 수 있게 됐다는 것이다. 그리고 텍스트 인덱스를 만들고 싶다면, 마침 나는 Bing의 검색 인덱스인 BitFunnel — 지속적이고 빠른 텍스트 수집에 특화되어 SIGIR에서 최우수 논문상을 받은 작업을 한 적이 있어, 우리 기계 전체에 대한 빠른 로컬 인덱스를 만든다면 시도해 볼 만한 실험 몇 가지를 떠올릴 수 있다(내가 본 프로젝트들은 코드 디렉터리만 인덱싱하려는 것 같지만, 내 기계 성능을 정말로 망치는 건 codex가 생성된 파일이 가득한 거대한 임시 디렉터리에 대해 ripgrep을 실행하고, 거기서 찾지 못하면 내 기계 전체를 뒤지기 시작할 때이므로, 일부 프로젝트의 코드만이 아니라 내 디스크 전체에 대한 인덱스를 원한다).

내가 AI 랩에서 일하면서 Cerebras 칩이나 tok/s를 크게 높여 검색에 대한 부하/수요를 늘리는 다른 가속기에서 돌아가는 SOTA 모델 같은 것에 접근할 수 있다면, 기존 인덱서들을 조사해 충분히 빠른지, 아니면 직접 맞춤형으로 만들 필요가 있는지 살펴볼지도 모른다. BitFunnel의 오픈소스 버전은 바이트코드 인터프리터와 하나의 JIT만을 “겨우” 포함하지만, Bing 버전은 여러 개의 JIT 컴파일러를 포함한다. 그 수준의 최적화를 하는 프로젝트는 예전에는 엄청난 작업이었지만, “주말이면 할 수 있다”는 말이 이제는 이런 종류의 프로젝트 중 일부에서는 실제로 사실이 됐다. 월 200달러짜리 초라한 계정을 쓰는 내 입장에서는 조금 더 빨라진 ripgrep에 시중의 인덱스 하나를 더하는 정도면 충분하다고 생각하므로, 이 빠른 수집이 가능한 전체 머신 인덱스 프로젝트는 “독자( AI 랩에 다니는 독자)를 위한 연습 문제”로 남겨두자.

최적화는 저렴해졌다

최적화 비용이 급격히 떨어졌다는 사실은 공개 모델 기준으로도 이미 2025년 11월부터, 어쩌면 그보다 조금 더 전부터 사실이었다(그리고 AI 랩 내부에서 접근 가능했던 것을 생각하면 그보다 더 일렀을 것이다). GPT-5.1이나 5.2 시절의 예를 들자면, 게임 AI에 대한 지식이 전혀 없는 상태에서 Azul AI를 만들어 본 적이 있다. 결과적으로 그 AI는 꽤 큰 차이로 세계에서 가장 강력한 AI가 됐다. 두 번째로 강력한 AI를 설명하는 논문을 읽어보면, 내 AI가 “AI” 측면에서도 아마 조금 더 낫다고 생각하지만, 주로 승리한 지점은 최적화다. 논문에 나온 과정과 내 과정을 비교해 추정해 보면 아마 두 자릿수 규모로 적은 시간을 들였고, 게다가 주로 노트북에서 작업하면서 머신 클러스터를 쓸 수 있었던 상대와 달리(즉 실험을 돌리고 파라미터 튜닝을 할 대역폭이 훨씬 적었다는 뜻이다) 그랬다. 예를 들어, 그 다른 AI는 싱글 스레드인 반면 내 AI는 멀티 스레드다. 네이티브 코드 버전과 끔찍한 shared wasm memory + javascript 버전을 둘 다 가지고 있고, 두 가지 버전에 대해 완전히 다른 두 가지 탐색 아키텍처(매우 작고 빠른 넷을 위한 minimax와 더 큰 넷을 위한 MCTS)를 쓰므로, “필요한” 멀티스레딩 알고리즘도 완전히 다르다. 이걸 손으로 했다면 꽤 큰 작업이 됐을 것이다. 그리고 LLM이 자신의 (틀린) 추론에 기반해 멀티스레딩 알고리즘을 고르도록 몇 번 둔 뒤에야 직접 게임 AI용 멀티스레딩 알고리즘에 대해 30분 정도 읽어봤기 때문에, 결국 멀티스레딩 알고리즘을 여러 번 다시 작성하게 됐다(codex에게 다시 작성시키게 됐다).

이런 종류의 것을 위해 멀티스레딩 알고리즘을 디버깅하고 검증하려면 해야 할 표준적인 작업들이 있다. 예를 들면 알고리즘이 비결정적임에도 디버그 로그로부터 리플레이를 구현해 버그를 재현할 수 있게 하는 것이다. 그 작업만 손으로 했다면 아마 며칠에서 일주일은 걸렸을 텐데, 에이전트가 루프를 돌며 처리하기에는 딱 맞는 종류의 일이다(그냥 로그를 리플레이해보게 하고, 완벽한 리플레이가 되지 않을 때마다 비결정성에 대한 로깅을 삽입하면 된다). 이런 까다로운 최적화를 제대로 동작하게 만드는 데 필요했던 많은 지루한 작업이 사라졌다.

이는 다른 많은 까다로운 최적화에도 적용된다. CPU 마이크로코드를 작성하고, CPU 검증을 하고, 검색 엔진 인덱스를 최적화하는 등의 일을 해 보면서, 나는 최적화를 보며 “음, 이건 성능을 2% 높여주겠지만, 이 까다로운 최적화가 동작한다는 걸 검증하는 데 N인일이 걸리겠네”라고 생각하고 그 최적화를 할 가치가 있는지 판단하곤 했다. 이제 이 N이 엄청나게 떨어졌으므로(사람 시간 기준으로는 흔히 1000배/10000배/1000000배, 달러 비용으로는 토큰을 정량 요금제로 쓸 때의 비용과 BitFunnel 같은 검색 인덱스가 쓰던 JIT 컴파일러를 작성한 Bing 엔지니어의 비용을 비교하면 아마 1000배 정도), 시도할 만한 이런 종류의 최적화 수가 크게 늘었다. 제대로 동작할지 확신할 수 없는 최적화도 마찬가지다. 예전에는 최적화가 실제로 속도를 높여줄지 확신할 수 없을 때 “성능 영향을 어림짐작할 수 있을 정도로 구현해 측정하는 데 M시간이 걸리겠네”라고 생각하곤 했다. 이제는 그런 최적화 중 훨씬 더 많은 것을 시도해 볼 만하다.

게임 AI 사례로 돌아가면, 적어도 내가 시도한 AI에서는 속도가 두 배가 될 때마다 약 100 Elo를 얻는 것 같다(무승부가 매우 드물어서 체스보다 더 크다고 짐작한다). 멀티스레딩만 추가해도 큰 머신에서는 그 외에는 비슷한 AI를 완전히 압도하기에 충분하다. 손으로 하기엔 너무 귀찮다고 대부분이 여기는 10~20개의 최적화를 더 쌓으면, 강함의 차이는 엄청나고 손으로 만든 AI로 따라잡으려는 건 그리 현실적이지 않다3.

게임 AI 사례는 대부분의 소프트웨어보다 조금 더 귀찮은데, 하고 싶은 최적화 중 많은 것이 실제로 결과를 바꾸고, 속도 증가와 결과 변화가 합쳐져 실제로 더 좋은지 나쁜지를 알려주는 값싸고 간단한 방법이 없기 때문이다. 그리고 앞서 언급한 바와 같이, 현재 공개된 SOTA 모델들은 실험 설계에 꽤 서툴러서, 나는 최적화가 좋은지 판단하는 데 쓰는 프레임워크를 직접 마련해야 했지만, 일단 그게 갖춰지자 다른 최적화 문제와 다를 바 없었다. LLM 최적화를 하는 사람들도 이런 종류의 문제는 다뤄야 할 것 같지만, 대부분의 최적화 문제는 훨씬 더 간단하다.

다른 예를 하나 들자면, 성능 인터뷰를 준비하면서 Jamie Brandon은 Anthropic의 이제 공개된 성능 과제를 시도해 봤다. 직접 시도한 뒤, 그가 멈춘 지점부터 Claude에게 이어서 하게 했더니 훨씬 더 좋은 결과가 나왔다. Claude가 자신이 하지 않은 것 중 무엇을 했는지 살펴보니, 많은 최적화가 그에게도 떠올랐지만 아직 손을 대지 못한 것들이었고, “[o]thers were just crazy shit that I would never try unless I was working on this for weeks”4 — “다른 것들은 내가 이걸 몇 주 동안 붙잡고 있지 않는 한 절대 시도하지 않을 미친 짓이었다”는 것이었다. 그는 합리적인 성능 엔지니어이고 원하는 성능 직군에 대한 오퍼를 받았지만, 잘 정의된 최적화 문제에서는 괜찮은 모델을 이길 가망이 없다(나도 그 문제를 직접 시도해 보진 않았지만, 비슷한 시간 제한이라면 나 역시 가망이 없을 것 같다).

워크로드 특화 최적화

특정 워크로드 하나에 맞춰진 동적 맞춤형 소프트웨어 — 워크로드의 한 부류가 아니라 — 가 매우 가능성 높은 결과가 될 것 같다.

이는 꽤나 필연적으로 보인다. 내 글에 대한 또 다른 답글에서 pgrust의 Michael Malis는 비슷한 말을 했다:

[pgrust 최적화에 대한 논의] ... 이런 최적화를 만드는 것이 충분히 쉽기 때문에 고객의 워크로드를 보고 필요에 따라 추가할 수 있다고 생각한다

별다른 프레임워크나 준비 없이, 이 글을 쓰기 시작하기 직전에 나는 에이전트에게 내 ripgrep 쿼리(네이티브 코드 컴파일러 전환이 아니라, 벤치마크 세트에 기반한 일반 FRE 엔진에 대한 최적화)에 대해 워크로드 특화 최적화를 시켰는데, 실행하는 데 내 시간 2분 정도밖에 걸리지 않았다. 최적화는 쿼리 세트에 대해 실행되고, 나중에 홀드아웃 쿼리 세트에 대해 실행한다. 아직 실행 중이지만 초기 결과는 꽤 유망해 보인다. 한 번의 최적화 패스 뒤에, 워크로드 최적화 버전은 홀드아웃에서 표준 ripgrep보다 2% 빠르고 아직 더 빨라지고 있다. 내 로컬 ripgrep 사용량에는 2%가 큰 의미는 없지만, 이게 몇 분 만에 이루어졌고 여기서 한 최적화가 내가 이 문장을 타이핑하기 시작할 때 시작되어 아직 개선되고 있다는 점을 생각하면 2%의 이득도 기꺼이 받겠다(참고로 이건 네이티브 코드 컴파일러와 결합된 것이 아니며, 제대로 결합하면 전체 이득은 더 커질 것이다). 그리고 이는 FRE 정규식 엔진5을 활용한 것임을 기억하자. FRE는 홀드아웃 벤치마크에서 Rust 정규식 엔진보다 상당히 느렸고, 정규식 워크로드에 대해 아는 것이 전무한 나와 SOTA LLM이 유도 없는 개방형 자기 개선 루프에서 실험 설계가 충분히 뛰어나지 못해 홀드아웃 성능 개선이 정체된 상태였다. 하지만 내가 신경 쓰는 것이 내 워크로드에서의 성능이라면, 나는 충분한 데이터를 가지고 있고 계속해서 더 많은 데이터를 생성하고 있다. 위에서 Marc Brooker가 언급했듯이, 옛 데이터에 없는 체제 변화가 있을 때 과적합에 주의해야 하는 건 맞지만, 그래도 이전보다 더 나은 상황에 있는 셈이다.

더 일반적인 경우, Amazon의 Marc Brooker나 pgrust를 작업하는 Michael Malis 같은 사람이라면, 이를 일회성으로 하는 데 그치지 않고 고객과 협력해 그들의 데이터를 활용해 최적화하는 프로그램을 시범 운영하고 이를 일반 고객에게 확장하는 방법을 고민하는 것이 타당하다. 나는 그런 일이 내 시간을 가장 잘 쓰는 일인 회사에서 일하고 있지는 않지만6, 더 큰 규모를 가진 회사들에게는 이런 일이 오고 있다는 걸 알 수 있다는 게 꽤 놀랍고, 내 개인 워크플로에 대한 이런 실험을 실행하는 데 내 시간이 몇 분밖에 걸리지 않는다는 점을 고려하면 개인 프로젝트에서 이런 종류의 것을 만지작거리는 것도 꽤 합리적이다.

Jamie Brandon, Michael Malis, andrea (@s__video), Artyom Bologov, Max Bittker에게 논평/교정/논의에 감사드린다.

P.S. 지난 몇몇 글에서 언급했듯이, 코딩 에이전트와 함께라면 실험을 실행하고 호기심을 충족할 만큼의 결과를 보는 데 걸리는 시간은 크게 줄어든 반면, 결과를 정말 엄밀하게 만드는 데 걸리는 시간은 그대로거나 오히려 늘었기 때문에, 예전처럼 글을 쓴다면 내가 가진 대역폭에 비해 실행할 수 있는 실험 수가 매우 적어질 것이다. 그래서 나는 이런 실험들을 그냥 실행해서 몇몇 친구들과 결과만 공유해 왔다. 실험 삼아, 이런 것들을 매우 빠르고 비엄밀한 방식으로 작성해 보려고 한다. 지난 글처럼, 이 글을 쓰고 모든 정리를 30분 안에 끝내는 것을 목표로 했는데, 시간은 재지 않았지만 아마 조금 넘긴 것 같다.

이렇게 해도 이런 글을 쓰는 데 걸리는 시간은 길어서 최근 결과를 공유하는 데 뒤처지고 있지만, 아직은 LLM이 쓴 글로 바꾸고 싶지 않고(아직은?), 데이터 정리와 이런 글을 쓰는 데 걸리는 시간을 현실적으로 30분 이하로 줄일 수 있을 것 같지도 않다. 이 글의 길이만 봐도, 타이핑하는 데만 생각하며 멈추는 시간을 포함해 20~30분 정도 걸려야 하고, 그다음 데이터를 살펴보다가 뭔가 잘못된 것처럼 보이는 부분이 있으면 문제를 고쳐야 하는지 더 자세히 들여다봐야 한다(이 글에서도 여러 번 그런 일이 있었고, 시간을 많이 더 쓰지 않았기 때문에 내가 모르는 다른 데이터 문제도 있을 것으로 예상한다).

어쨌든, 이런 빠르고 (분명 더 부정확한) 글쓰기에 대해 의견이 있다면 알려 달라 (X Bsky Mastodon)!

부록: 이제 소프트웨어가 느릴 이유가 없다

나는 X 개발자들은 형편없고 반성해야 한다는 일반적인 정서에 강력히 반대한다는 입장을 오랫동안 밝혀 왔다. 느린 코드를 작성했다는 이유로 — 프로그래밍 전문성에는 여러 종류가 있기 때문이다. 대부분의 프로그래머가 성능 전문성을 갖고 있지 않은 것은 물론, 그것을 키우는 것 자체가 말이 되지 않는 경우가 많다는 점(사업이 중요하게 여기는 것, 고용 시장의 모습 등을 고려할 때)에서 그렇다. 그러니 물론 대부분의 프로젝트가 성능 전문가가 할 수 있는 것에 비해 성능이 매우 떨어질 수밖에 없다. 성능 전문가가 점점 벌어지는 간극프로그램이 얼마나 빨라질 수 있는지와 실제로 얼마나 빠른지 사이의 간극 — 을 보고 터무니없다고 생각하는 이유를 이해한다. 나도 그 안에 부조리가 있다는 점에는 동의하지 않는 것이 아니지만, 프로그램이 얼마나 좋은 UI를 가질 수 있는지와 내가 손으로 만들 수 있는 UI가 얼마나 좋은지 사이의 간극을 생각해 보면, 그것 역시 덜 부조리해 보이지 않지만, 그렇다고 해서 내가 훌륭한 UI — 심지어 괜찮은 UI — 를 만드는 법을 배우는 데 시간을 쓰는 것이 말이 된다고 생각하지도 않는다. 성능에 대해 대부분의 사람들이 시간을 들여 배우는 것이 말이 되지 않는 것과 같은 이유다.

위의 예에서, Jamie Brandon은 Anthropic으로부터 오퍼를 받았고 아마 OpenAI가 아니면 그를 고용할 여유가 없을 테지만, 한정된 최적화 문제에서 그를 이길 수 있는 코딩 에이전트를 쓰는 비용은 감당할 수 있다. 에이전트는 그만큼의 판단력을 갖고 있지 못해 개방형 문제에서는 더 못하겠지만(최적화된 정규식 엔진을 만들면서 그냥 과적합하지 말라고만 했을 때, 홀드아웃 벤치마크에서 최고의 정규식 엔진보다 한 자릿수 이상 느렸던 것을 기억하자. 하지만 홀드아웃에서 성능이 떨어진다고 말해준 뒤에는 에이전트가 정규식 엔진 성능을 충분히 빠르게 만들어 일반적으로 2군 정규식 엔진 수준의 성능에 맞먹게 했는데, 이는 오늘날 대부분의 코드에 존재하는 일반적인 성능 최적화 수준과 비교하면 여전히 엄청나게 좋은 것이다), 그래도 온갖 문제에서 합당한 성능을 달성하기에는 충분히 좋다. 이 글에서는 주로 백엔드 성능 문제를 논했지만, LCP나 CLS 같은 지표들을 낮추고 싶다면 에이전트가 프론트엔드 성능에서도 못하지는 않아 보인다. 실제로 최근 글에 인터랙티브 플롯을 넣은 뒤 클라이언트 측 성능 수치가 나빠진 것을 발견하고, LLM에게 주간 할당량의 1%를 써서 최적화하게 했더니 수치가 다시 좋아졌다. 아주 단순한 사이트지만, 이런 종류의 최적화를 수백만 사용자에게 배포되는 꽤 복잡한 앱에서도 사람들이 하고 있고 거기서도 통한다. 다만 토큰이 조금 더 들 뿐이다.

나는 여전히 누군가의 소프트웨어 성능이 떨어진다고 해서 그 사람이 형편없고 반성해야 한다고 생각하지 않는다. 하지만 성능에 대해 아무것도 모르는 사람이라도 LLM을 제대로 활용할 줄 아는 사람이라면(성능 문제에 특화된 활용이 아니라 일반적으로) 대체로 괜찮은 성능을 내는 소프트웨어를 만들 수 있어야 한다고 생각한다. LLM에게 그냥 최적화하라고만 하면 종종 매우 잘못된 일들을 많이 해서 직접 잡아내야 하지만, 그건 애초에 LLM을 효과적으로 쓰는 것 자체가 일반적으로 그런 것이므로, 이제는 괜찮은 성능을 얻는 것이 더 이상 전문 기술이 아니다.

부록: codex는 ripgrep을 어떻게 실행하고 있나?

내 기계에서 ripgrep 쿼리 분포에 대한 정보는 다음과 같다. 이것이 다른 어디에서 일어나는 일에 대해 전혀 대표성이 있다고 주장하지는 않는다. 검색되는 패턴 길이의 패턴 분포를 보면 내가 예상했던 것보다 긴 패턴이 훨씬 많다. p50이 55개의 유니코드 코드 포인트(이 글에서는 단순화를 위해 그냥 문자라고 부르겠다)인데, 이는 이미 내가 손으로 grep하는 것보다 길고, p90은 119다!

정규식에서 alternation 분기 수 역시 내가 손으로 하는 것보다 훨씬 더 복잡하다.

또 다른 관점으로 이 둘의 상관관계를 살펴보자. 정규식이 길어질수록 alternation 분기도 많아지는가? 그렇다.

그렇다면 이 정말 긴 정규식들은 대체 무엇인가? 살펴보면, 가장 긴 것들의 대부분은 함수나 테스트 이름에 대한 긴 alternation이며, 예를 들어 다음 정규식은 FRE 개발과 관련된 것으로 보인다.

  fn (hot_byte_compiler_is_generic_only_and_anonymous_count_uses_auto_count|
  one_pattern_count_spans_uses_the_retained_complete_span_session|
  formal_compact_state_byte_visitors_coexist_with_native_count|
  fixed_boundary_record_visit_matches_line_relative_reference_and_is_atomic|
  unbounded_languages_refuse_finite_extraction_before_allocation|
  formal_single_raw_span_sweep_preflight|
  assert_exact_fixture_uses_formal_large_continuation_sweep|
  url_only_compile_identity_binds_language_and_owner_mode|
  url_only_compile_exact_limits_and_runtime_refusals_close|
  url_only_compile_post_plan_allocation_faults_close|
  url_only_owner_discriminator_is_stable_and_precharged|
  url_only_compile_owner_is_strategy_and_operation_scoped|
  formal_rebar_url_owner_is_compile_only_and_matches_oracle|
  formal_rebar_url_exact_fixture_uses_certified_execution|
  formal_fixed_schema_materialization_matches_both_record_oracles_and_controls|
  formal_single_count_selects_compact_state_byte_complete_bound_visitors|
  authenticated_bound_line_total_lf_free_domain_opportunity_exceeds_five_percent|
  prepared_absolute_onepass_fuses_slots_and_preserves_pre_source_fallback|
  authenticated_word_boundary_russian_compact_lowering_public_canary|
  ordered_nfa_x86_epsilon_edges_bypass_the_assertion_call|
  ordered_nfa_aarch64_epsilon_edges_bypass_the_assertion_call|
  ordered_edge_dispatch_v2_is_target_neutral_deterministic_and_relocation_free|
  ordered_edge_dispatch_v2_copies_canonical_tables_and_cap_falls_back_to_v1|
  ordered_nfa_v3_composes_terminal_range_and_dispatch_without_data_relocations|
  ordered_nfa_x86_terminal_range_emits_authenticated_reverse_scan|
  ordered_nfa_aarch64_terminal_range_emits_authenticated_reverse_scan|
  ordered_nfa_x86_boundary_assertion_cache_is_lazy_and_boundary_scoped|
  ordered_nfa_aarch64_caches_repeated_assertions_once_per_boundary|
  boundary_assertion_cache_requires_dense_exact_kind_reuse|
  boundary_assertion_cache_selection_is_compiler_only_and_deterministic)

하지만 일부는 재미있는 숫자 구성이다. 예를 들면 다음과 같다

:(13[0-9]|14[0-9]|15[0-9]|16[0-9]|17[0-9]|18[0-9]|19[0-9]|20[0-9]|21[0-9]|22[0-9]|23[0-9]|24[0-9]|25[0-9]|26[0-9]|27[0-9]|28[0-9]|29[0-9]|30[0-9]|31[0-9]|32[0-9]|33[0-9]|34[0-9]|35[0-9]|36[0-9]|37[0-9]|38[0-9]|39[0-9]|40[0-9]|41[0-9]|42[0-9]|43[0-9]|44[0-9]|45[0-9]|46[0-9]|47[0-9]|48[0-9]|49[0-9]|50[0-9]|51[0-9]|52[0-9]|53[0-9]|54[0-9]|55[0-9]|56[0-9]|57[0-9]|58[0-9]|59[0-9]|60[0-9]|61[0-9]|62[0-9]|63[0-9]|64[0-9]|65[0-9]|66[0-9]|67[0-9]|68[0-9]|69[0-9]|70[0-9]|71[0-9]|72[0-9]|73[0-9]|74[0-9]|75[0-9]|76[0-9]|77[0-9]|78[0-9]|79[0-9]|80[0-9]|81[0-9]|82[0-9]|83[0-9]|84[0-9]|85[0-9]|86[0-9]|87[0-9]|88[0-9]|89[0-9]|90[0-9]|91[0-9]|92[0-9]|93[0-9]|94[0-9]|95[0-9]|96[0-9]|97[0-9]|98[0-9]|99[0-9])[0-9]:

이는 :(?:1[3-9]|[2-9][0-9])[0-9]{2}:와 동등하며(원본 입력에 대해 ripgrep으로 실행하면 성능이 거의 같다. 더 짧은 정규식이 실제 쿼리 데이터에서는 기술적으로 조금 더 빠르긴 하지만 아주 미세한 차이일 뿐이다). 전체 파이프라인은 다음과 같았다

cargo clippy … | rg 'crates/fre-aot-regex/src/module.rs:' | rg NUMBER_REGEX | head -250

사람이 하기엔 다소 이상한 일일 수도 있지만, 에이전트들은 이런 종류의 일을 늘 하는 것 같다.

다른 주제로, ripgrep 쿼리가 얼마나 오래 걸렸는지 살펴보면 꽤 많은 느린 쿼리가 있다. 예를 들어 p99는 거의 1분이다! 그리고 p999는 거의 10분이다! 그리고 이 기간(약 한 달 동안 한 노트북에서; AWS 호스트에서 에이전트를 돌릴 때의 쿼리와 분포는 다를 것으로 보이지만 확인해 보진 않았다) 동안의 최대 쿼리는 2시간에 육박한다!

명령줄 옵션 측면에서는 다음과 같은 분포를 보인다. 예상대로 codex는 종종 줄 번호를 원하고, 어째서인지 아주 가끔 PCRE2 정규식을 사용한다.

이에 대한 플롯이나 표는 추가하지 않겠지만, 또 하나 주목할 점은 검색되는 패턴의 지역성이 꽤 낮다는 것이다(패턴의 약 94%는 한 번만 등장했다). 이는 많은 쿼리가 매우 길었다는 점을 고려하면 어느 정도 이해가 된다. 하지만 검색되는 파일에 대해서는 지역성이 꽤 높고, 한 번 검색된 파일은 곧 다시 검색될 가능성이 상대적으로 높으므로(파일이 충분히 작다면) 메모리에 있는 상태에서 검색될 가능성이 높다는 것을 시사한다.

또한, 쿼리의 99%는 정규식 쿼리였고(1%는 정규식이 아닌 문자열 검색이었다) 검색 쿼리의 99.9%는 ASCII 전용이었지만, 검색된 파일 측면에서는 약 45%가 ASCII 전용이었고 55%가 유니코드를 포함하고 있어, 내가 예상했던 것보다 유니코드 비율이 높았다.

지난 글의 초안에 대해 Peter Geoghegan은 이렇게 지적했다

정규식 구현이 더 적은 기능을 지원함으로써 더 빨라지는 것도 가능하다. 일부 구현은 백레퍼런스 등을 지원하지 않는다.

이는 여기서도 마찬가지다. 여기서 수행된 워크로드 특화 최적화는 내가 codex에 짧은 지시만 주고 하고 싶은 대로 하게 했기 때문에 꽤 피상적이었고(일반적으로 codex를 가장 효과적으로 쓰는 방법은 아니다), 더 자세한 계획을 세운다면 내 쿼리의 일반적인 사용 사례를 지원하는 더 집중된 최적화로 더 큰 이득을 기대할 수 있을 것이다.


  1. 물론 그 글에서 그리고 이전에도 논했듯이, SOTA 모델의 벤치마킹 및 실험 설계 기술은 에이전트를 위해 벤치마킹 환경을 구축하는 사람(또는 스킬) 없이는 일반적인 경우에 이를 수행할 만큼 충분히 좋지 않다. [return]
  2. 우리의 예전 벤치마크에서 볼 수 있듯이, 컴파일러를 실행할 시간을 감안하더라도 네이티브 코드 컴파일 버전이 Rust 정규식 크레이트보다 느린 경우가 많이 있다. 왜 그런지 살펴보면, 이들은 Rust 정규식 크레이트가 어떤 알고리즘적 최적화를 가지고 있는데 FRE 네이티브 코드 컴파일러가 단순한 방식으로 폴백하는 더 복잡한 쿼리인 경향이 있다( FRE를 만든 에이전트는 “일반” 정규식 엔진에 들인 시간보다 네이티브 코드 컴파일러에 훨씬 적은 시간을 들였다). [return]
  3. 손으로 쓴 AI가 Go와 체스에서 세계 최고 수준의 엔진을 작성한 사람처럼 진짜 AI 전문성을 가진 사람에 의해 만들어졌다면 “AI” 측면이 더 뛰어나서 내 AI를 이길 수 있다는 점에는 의심의 여지가 없지만, 전문성 수준이 어느 정도 비슷하다면 주어진 시간 대비 LLM으로 작성된 버전이 압도할 것이다. [return]
  4. 그가 멈춘 지점에서 에이전트가 이어서 하도록 한 결과를 비교하는 것은 불공정하다고 볼 수도 있는데, 그의 작업이 에이전트가 스스로 할 때보다 훨씬 더 잘할 수 있게 하는 출발점이 되기 때문이다. 그래서 나는 새로운 과제를 에이전트에게 그대로 주었고, 그의 작업을 재활용했을 때와 매우 유사한 점수를 얻었다(그리고 다른 에이전트에 의한 빠른 검사에서도 부정행위의 증거는 찾지 못했다). [return]
  5. ripgrep 포크를 직접 수정하도록 에이전트를 시켰다면 성능이 더 좋았을 수도 있지만, 이것이 내 쿼리에 대한 FRE의 과적합 문제도 해결할 수 있는지 궁금했다. [return]
  6. 얼마 전, 우리 가입 플로우의 페이지 크기를 50MB에서 5MB로 줄였고 수익 A/B 테스트에 따르면 수익이 약 0.5% 증가한 것으로 나타났다. 일반적으로 나는 이런 것처럼 간단하고 쉬운 승리를 먼저 하는 것을 크게 선호하며, 맞춤형 컴파일러를 구축하거나 다른 고도로 전문화된 기술 작업을 하는 것보다 ROI가 더 높은 승리들이 아마도 많이 있을 것이다. [return]

이 글은 muse-spark-1.2-contributor 모델을 사용해 번역했습니다.

댓글