Vim :g//d가 대용량 파일에서 멈추는 진짜 이유와 우회법
- 공유 링크 만들기
- X
- 이메일
- 기타 앱
Vim :g//d · :v//d가 대용량 파일을 얼어붙게 만드는 원인
텍스트 에디터 · 성능 분석 · Vim/Neovim 비교
수십만~수백만 줄 텍스트에서 Vim의 :g/패턴/d로 줄을 지우려다 고성능 PC가 순간 멈춰버린 경험, 한 번쯤 있을 것이다. 명령 한 줄은 단순한데 시스템이 통째로 잠기는 이 현상의 정체는 사실 명령 뒤에 숨은 수십만 번의 부수 작업 반복이다. 그리고 해법은 의외로 편집기를 Neovim으로 바꾸는 것이 아니라, 작업 방식 자체를 바꾸는 데 있다. 원인 → 우회법 → Neovim 비교 순서로 끝까지 풀어본다.
🧩 한눈에 보는 결론 — 멈추는 원인은 d 명령이 줄 수만큼 개별 발사되며 매번 클립보드·레지스터·undo를 건드리는 데 있다. 즉시 해법은 블랙홀 레지스터, 궁극 해법은 grep/rg/sed 같은 외부 스트림 필터다. Neovim 전환은 답이 아니다.
기초 — :global 명령은 실제로 어떻게 동작하나
:g/pattern/d는 직관적으로 "패턴 들어간 줄을 다 지워라"라는 단일 명령처럼 보인다. 하지만 내부적으로는 2단계 패스(two-pass) 구조로 동작한다. 이 구조를 이해하는 것이 모든 문제의 출발점이다.
1차 패스 (마킹) — 파일 전체를 한 번 스캔하며 패턴에 매칭되는 줄마다 내부 플래그를 세운다.
2차 패스 (실행) — 마킹된 줄 각각에 대해 지정한 명령(d)을 개별적으로 반복 실행한다.
:v(또는 :g!)는 단지 "매칭되지 않은 줄"을 마킹할 뿐, 메커니즘은 완전히 동일하다. 핵심은 2차 패스다. 100만 줄에서 90만 줄을 지우면 d 커맨드가 문자 그대로 90만 번 발사된다.
graph LR
A[파일 전체 스캔] --> B[패턴 매칭 줄
내부 플래그 마킹]
B --> C[마킹된 줄마다
d 명령 개별 실행]
C --> D[90만 줄이면
90만 번 반복]
style A fill:#e8f8f5,stroke:#16a085
style B fill:#fef9e7,stroke:#f39c12
style C fill:#fdedec,stroke:#e74c3c
style D fill:#fdedec,stroke:#c0392b,color:#c0392b
🔗 다이어그램 요약: :global은 파일을 한 번 스캔해 매칭 줄을 마킹한 뒤, 마킹된 줄마다 삭제 명령을 개별 반복한다. 줄이 많을수록 반복 횟수가 그대로 폭증해 90만 줄이면 90만 번의 삭제가 줄지어 실행된다.
원인 분석 — 왜 90만 번의 삭제가 시스템을 잠그는가
문제는 삭제 횟수 자체가 아니다. Vim이 매 d마다 부수 작업을 겹겹이 동반한다는 데 있다. 누적되는 4가지 오버헤드를 효과가 큰 순서로 정리한다.
① 시스템 클립보드 반복 동기화 — 가장 치명적
set clipboard=unnamed(또는 unnamedplus)가 켜져 있으면, Vim은 삭제된 텍스트를 매번 OS 클립보드에 복사한다. 이 OS 클립보드 API 호출이 수십만 번 반복되는 것 자체가 시스템을 잠근다. Vim 개발자 메일링 리스트에서 직접 확인된 200,000줄에서 150,000줄을 삭제한 실측은 다음과 같다.
동일 작업, 설정 하나로 갈리는 처리 시간 비교다 (값이 작을수록 빠름):
즉 같은 작업이 설정 하나로 수백 배 차이가 난다. gVim(GUI)은 특히 Windows에서 이 경로가 극단적으로 느리다. 클립보드 동기화는 단순 메모리 복사가 아니라 OS와의 통신을 동반하므로, 반복 횟수가 늘수록 비용이 비선형으로 폭증한다.
② 번호 레지스터 순환("1~"9)
Vim은 삭제 내용을 번호 레지스터에 보관하며, 매 삭제마다 "1→"2→…"9로 내용을 밀어내는 rotate 연산을 한다. 단일 메모리 복사지만 수십만 번 누적되면 무시할 수 없는 오버헤드가 된다.
③ Undo 트리 누적
:g//d는 u 한 번으로 전체를 되돌릴 수 있는 것처럼 보이지만, 내부적으로는 개별 삭제마다 undo 트리에 엔트리가 추가된다. 수백만 줄 삭제 시 트리가 수십~수백 MB를 점유하고, 메모리 할당·GC 압력이 실시간 인터랙션을 지연시킨다.
④ 신택스 하이라이팅 재평가 + swap 갱신
줄이 지워질 때마다 syntax 엔진이 주변 컨텍스트를 재파싱하고, swap 파일에 변경을 기록한다. 파일이 클수록 "어디서부터 다시 파싱할지" 탐색 비용이 커진다. 이 넷이 곱해지면서, 단일 코어로 처리되는 동기 루프가 CPU·메모리·I/O를 동시에 점유해 "고성능 기계도 멈추는" 체감 버벅임을 만든다.
우회 방법 — 효과 순서대로
원인을 알면 해법은 명확하다. 반복에 딸려오는 부수 작업을 끊거나, 반복 자체를 외부로 빼내면 된다. 효과가 큰 순서로 6가지를 정리한다.
방법 1 · 블랙홀 레지스터 (즉시 적용)
삭제 내용을 레지스터·클립보드 어디에도 저장하지 않는 특수 레지스터 "_를 쓴다. 번호 레지스터 rotate와 클립보드 동기화가 동시에 사라져 체감 차이가 가장 크다.
:g/pattern/d _ " 더 간단한 형태
방법 2 · 작업 전 clipboard 임시 해제 (gVim Windows 특효)
:g/pattern/d
:set clipboard=unnamed " 작업 후 복구
방법 3 · undo 임시 비활성화 (메모리 급감)
:g/pattern/d
:set undolevels=1000 " 복구
⚠️ 이 구간은 u로 되돌릴 수 없으므로 반드시 백업 후 사용한다.
방법 4 · 외부 필터로 파이프 (수백만 줄에 최적)
:%!는 버퍼 전체를 외부 프로세스에 넘기고 출력으로 교체한다. grep/sed/awk는 스트림 처리에 최적화돼 Vim 내부 루프보다 수 배~수십 배 빠르다.
:%! grep "pattern" " 매칭 줄만 남김 (= :v/.../d)
:%! rg "pattern" " ripgrep, 더 빠름
:%! sed '/pattern/d'
:%! awk '!/pattern/'
방법 5 · 편집기 밖에서 전처리 후 열기 (근본책)
파일을 한 번만 순차 읽어 새 파일로 쓰므로 undo/register 조작이 아예 없다. ripgrep은 13.5GB 기준 grep 대비 약 6배 빠른 것으로 벤치마크된다.
rg -v "pattern" big.txt > out.txt " 대용량 최속
방법 6 · vimrc 대용량 자동 최적화
LargeFile 플러그인(vim.org #1506) 또는 BufReadPre에서 일정 크기 초과 시 undolevels=-1, noswapfile, syntax off를 자동 적용하면 앞으로 큰 파일을 열 때마다 위 설정이 자동으로 걸린다.
지금 당장 어떤 방법을 고를지는 줄 규모 하나로 갈린다.
flowchart TD
A([대용량 g//d가 멈춤]) --> B{줄 규모는?}
B -->|수만~수십만| C[블랙홀 레지스터
:g/pattern/d _]
B -->|수백만 이상| D[외부 필터
:%! grep -v]
C --> E([빠르게 완료])
D --> E
style A fill:#3498db,stroke:#2980b9,color:#ffffff
style B fill:#fef9e7,stroke:#f39c12
style C fill:#eafaf1,stroke:#27ae60,color:#1e8449
style D fill:#eafaf1,stroke:#27ae60,color:#1e8449
style E fill:#3498db,stroke:#2980b9,color:#ffffff
🔁 다이어그램 요약: 줄이 수만~수십만이면 블랙홀 레지스터(:g/pattern/d _)로 충분하고, 수백만 이상이면 외부 필터(:%! grep -v)로 통째로 넘기는 것이 가장 빠르다. 두 경로 모두 부수 작업 반복을 끊어 즉시 완료로 이어진다.
Neovim으로 바꾸면 해결되는가 — 비교 검토
많은 사람이 기대하는 "Neovim으로 갈아타면 빨라지지 않을까?"의 답을 표로 정리했다.
| 항목 | Vim | Neovim |
|---|---|---|
| :global 내부 구현 | 2-pass 마킹 | 동일 (코드 상속) |
| clipboard 처리 | 내장 직접 | 외부 provider 위임 |
| 신택스 하이라이팅 | regex 전체 재평가 | Tree-sitter 증분 파싱 |
| 시작 속도 | ~28ms | ~12ms |
| 플러그인 실행 | 단일 스레드 | async / Lua 비동기 |
| :global 대용량 성능 | 느림 | 마찬가지로 느림 |
핵심은 명료하다. :global 명령은 Neovim이 Vim에서 그대로 상속한 코드라 근본 병목이 동일하다. Tree-sitter의 증분 파싱은 파일 표시(하이라이팅)에만 작동할 뿐 :global 루프 속도와는 무관하다. 오히려 Neovim 0.3.0이 특정 워크로드에서 Vim 8보다 40~62% 느렸다는 사례(이슈 #8657)도 보고됐다.
💡 결론: 편집기를 Neovim으로 바꿔도 :g//d 성능 문제는 그대로 따라온다. 해결의 축은 편집기 선택이 아니라 작업 방식, 즉 위의 우회 방법이다.
상황별 권장 — 한 장으로 끝내기
| 상황 | 권장 방법 |
|---|---|
| 지금 당장 빠르게 | :g/pattern/d _ (블랙홀) |
| gVim Windows에서 거의 멈출 때 | :set clipboard= 후 작업 |
| 수백만 줄, 속도 최우선 | :%! grep -v "pattern" |
| 파일을 아예 정제 | 셸에서 sed/rg 전처리 후 열기 |
| 대용량을 상시 다룬다면 | .vimrc에 LargeFile 자동 설정 |
🧠 한 줄 요약 — 느림의 정체는 명령이 아니라 명령에 딸려오는 클립보드·레지스터·undo의 수십만 번 반복이며, 가장 확실한 해법은 그 반복을 통째로 우회하는 외부 스트림 필터(grep/rg/sed)다. Neovim 전환은 이 문제의 해답이 아니다.
참고 자료
▶ Vim dev mailing list — clipboard 동기화 실측 사례
▶ LearnVim global command — 2-pass 구조 설명
▶ Vim Tips Wiki LargeFile — 대용량 자동 최적화
▶ ripgrep vs grep benchmark — 13.5GB 6배 차이
▶ Neovim vs Vim 2026 — 성능 비교
본 글은 공개된 Vim/Neovim 문서, 개발자 메일링 리스트, 벤치마크 자료를 바탕으로 정리한 정보 제공용 콘텐츠입니다. 실제 환경(OS·버전·플러그인 구성)에 따라 체감 성능은 달라질 수 있으니, 중요한 작업 전에는 반드시 파일을 백업하시기 바랍니다.
📄 Raw Data
## Vim `:g//d` · `:v//d`가 대용량 파일에서 시스템을 얼어붙게 만드는 원인과 우회 전략 ### 질문의 핵심 사용자가 묻는 것은 세 가지다. (1) 수만~수백만 줄 텍스트에서 `:g/패턴/d`, `:v/패턴/d`로 줄을 지우거나 살릴 때, 고성능 PC조차 순간적으로 멈추는 **근본 원인**. (2) 이 병목을 **우회하는 실용적 방법**. (3) **Neovim으로 바꾸면 해결되는가**의 비교 검토. 결론부터 압축하면 — 멈추는 원인은 명령 한 줄의 단순함 뒤에 숨은 *수십만 번의 부수 작업 반복*이고, 해결은 편집기 교체가 아니라 *작업 방식의 전환*에 있다. --- ### 기초 정보 — `:global` 명령은 실제로 어떻게 동작하는가 `:g/pattern/d`는 직관적으로는 "패턴 들어간 줄을 다 지워라"라는 단일 명령처럼 보이지만, 내부적으로는 **2단계 패스(two-pass)** 구조다. 1. **1차 패스(마킹)**: 파일 전체를 한 번 스캔하며 패턴에 매칭되는 줄마다 내부 플래그를 세운다. 2. **2차 패스(실행)**: 마킹된 줄 각각에 대해 지정한 명령(`d`)을 **개별적으로 반복 실행**한다. 여기서 `:v`(또는 `:g!`)는 단지 "매칭되지 **않은** 줄"을 마킹할 뿐, 메커니즘은 동일하다. 핵심은 2차 패스다. 100만 줄에서 90만 줄을 지우면 `d` 커맨드가 문자 그대로 **90만 번 발사**된다. 이 점이 모든 문제의 출발점이다 (출처: LearnVim global command). --- ### 원인 분석 — 왜 90만 번의 삭제가 시스템을 잠그는가 문제는 삭제 횟수 자체가 아니라, Vim이 매 `d`마다 **부수 작업을 겹겹이 동반**한다는 데 있다. 누적되는 4가지 오버헤드를 효과가 큰 순서로 정리한다. **① 시스템 클립보드 반복 동기화 — 가장 치명적** `set clipboard=unnamed`(또는 `unnamedplus`)가 켜져 있으면, Vim은 삭제된 텍스트를 매번 OS 클립보드에 복사한다. 이 OS 클립보드 API 호출이 수십만 번 반복되는 것 자체가 시스템을 잠근다. Vim 개발자 메일링 리스트에서 직접 확인된 사례로, 200,000줄에서 150,000줄을 삭제할 때: - `clipboard=unnamed`: **Windows에서 수 분, macOS에서 약 30초** - `clipboard=""`(해제): **2초 이내** 즉 같은 작업이 설정 하나로 수백 배 차이가 난다. gVim(GUI)은 특히 Windows에서 이 경로가 극단적으로 느리다 (출처: Vim dev mailing list, WebFetch 검증). **② 번호 레지스터 순환(`"1`~`"9`)** Vim은 삭제 내용을 번호 레지스터에 보관하며, 매 삭제마다 `"1`→`"2`→…`"9`로 내용을 밀어내는 rotate 연산을 한다. 단일 메모리 복사지만 수십만 번 누적되면 무시할 수 없는 오버헤드가 된다 (출처: LearnVim). **③ Undo 트리 누적** `:g//d`는 `u` 한 번으로 전체를 되돌릴 수 있는 것처럼 보이지만, 내부적으로는 개별 삭제마다 undo 트리에 엔트리가 추가된다. 수백만 줄 삭제 시 트리가 수십~수백 MB를 점유하고, 메모리 할당·GC 압력이 실시간 인터랙션을 지연시킨다 (출처: Vim Tips Wiki). **④ 신택스 하이라이팅 재평가 + swap 갱신** 줄이 지워질 때마다 syntax 엔진이 주변 컨텍스트를 재파싱하고, swap 파일에 변경을 기록한다. 파일이 클수록 "어디서부터 다시 파싱할지" 탐색 비용이 커진다. 이 넷이 곱해지면서, 단일 코어로 처리되는 동기 루프가 CPU·메모리·I/O를 동시에 점유해 "고성능 기계도 멈추는" 체감 버벅임을 만든다. --- ### 우회 방법 — 효과 순서대로 **방법 1 · 블랙홀 레지스터 (즉시 적용, 가장 간단)** ```vim :g/pattern/normal "_dd " 또는 간단히 :g/pattern/d _ ``` 삭제 내용을 레지스터·클립보드 어디에도 저장하지 않는다. 번호 레지스터 rotate와 clipboard 동기화가 동시에 사라져 체감 차이가 크다. **방법 2 · 작업 전 clipboard 임시 해제 (gVim Windows 특효)** ```vim :set clipboard= :g/pattern/d :set clipboard=unnamed " 작업 후 복구 ``` **방법 3 · undo 임시 비활성화 (메모리 급감)** ```vim :set undolevels=-1 :g/pattern/d :set undolevels=1000 " 복구 ``` 단, 이 구간은 `u`로 되돌릴 수 없으므로 **반드시 백업 후** 사용. **방법 4 · 외부 필터로 파이프 (수백만 줄에 최적)** ```vim :%! grep -v "pattern" " 매칭 줄 삭제 (= :g/.../d) :%! grep "pattern" " 매칭 줄만 남김 (= :v/.../d) :%! rg "pattern" " ripgrep, 더 빠름 :%! sed '/pattern/d' :%! awk '!/pattern/' ``` `:%!`는 버퍼 전체를 외부 프로세스에 넘기고 출력으로 교체한다. grep/sed/awk는 스트림 처리에 최적화돼 Vim 내부 루프보다 수 배~수십 배 빠르다. **대용량에서 가장 빠른 인-에디터 방법**이다. **방법 5 · 편집기 밖에서 전처리 후 열기 (근본책)** ```bash grep -v "pattern" big.txt > out.txt rg -v "pattern" big.txt > out.txt # 13.5GB 기준 grep 대비 약 6배 빠름 ``` 파일을 한 번만 순차 읽어 새 파일로 쓰므로 undo/register 조작이 아예 없다 (출처: codeant.ai ripgrep 벤치마크). **방법 6 · vimrc 대용량 자동 최적화** LargeFile 플러그인(vim.org #1506) 또는 `BufReadPre`에서 일정 크기 초과 시 `undolevels=-1`, `noswapfile`, syntax off를 자동 적용하면, 앞으로 큰 파일을 열 때마다 위 설정이 자동 걸린다. --- ### Neovim으로 바꾸면 해결되는가 — 비교 검토 | 항목 | Vim | Neovim | |------|-----|--------| | `:global` 내부 구현 | 2-pass 마킹 | **동일** (Vim 코드 상속) | | clipboard 처리 | 내장 직접 | 외부 provider 위임(pbcopy/xclip 등) | | 신택스 하이라이팅 | regex 전체 재평가 | Tree-sitter 증분 파싱 | | 시작 속도 | ~28ms | ~12ms | | 플러그인 실행 | 단일 스레드 | async/Lua 비동기 | | **`:global` 대용량 성능** | 느림 | **마찬가지로 느림** | 핵심: `:global` 명령은 Neovim이 Vim에서 그대로 상속한 코드라 **근본 병목이 동일하다.** Tree-sitter의 증분 파싱은 파일 *표시(하이라이팅)*에만 작동할 뿐 `:global` 루프 속도와 무관하다. 오히려 Neovim 0.3.0이 특정 워크로드에서 Vim 8보다 **40~62% 느렸다**는 사례도 보고된 바 있다(이슈 #8657). 다만 clipboard를 외부 provider에 위임하는 구조라, `clipboard=unnamed` 병목은 provider 구현에 따라 양상이 다소 다를 수 있다 (출처: Neovim vs Vim 2026). **결론: 편집기를 Neovim으로 바꿔도 `:g//d` 성능 문제는 그대로 따라온다.** 해결의 축은 편집기 선택이 아니라 위의 우회 방법이다. --- ### 결론 및 상황별 권장 | 상황 | 권장 방법 | |------|----------| | 지금 당장 빠르게 | `:g/pattern/d _` (블랙홀 레지스터) | | gVim Windows에서 거의 멈출 때 | `:set clipboard=` 후 작업 | | 수백만 줄, 속도 최우선 | `:%! grep -v "pattern"` (또는 `rg`) | | 파일을 아예 정제 | 셸에서 `sed`/`rg` 전처리 후 새 파일 열기 | | 대용량을 상시 다룬다면 | `.vimrc`에 LargeFile 자동 설정 | 한 줄 요약: **느림의 정체는 명령이 아니라 명령에 딸려오는 클립보드·레지스터·undo의 수십만 번 반복**이며, 가장 확실한 해법은 그 반복을 통째로 우회하는 *외부 스트림 필터(grep/rg/sed)*다. Neovim 전환은 이 문제의 해답이 아니다. --- ## References - [Vim dev mailing list](https://groups.google.com/g/vim_dev/c/PzKxIYNtgTo) - [LearnVim global command](https://learnvim.irian.to/basics/the_global_command/) - [Vim Tips Wiki LargeFile](https://vim.fandom.com/wiki/Faster_loading_of_large_files) - [ripgrep vs grep benchmark](https://www.codeant.ai/blogs/ripgrep-vs-grep-performance) - [Neovim vs Vim 2026](https://tech-insider.org/neovim-vs-vim-2026/)
댓글
댓글 쓰기