llvm

3 개의 포스트

github4분 읽기큐레이션 요약

일찍 멈추지 마라: 메모리 속도로 소스 코드의 대소문자를 정규화하기

GitHub는 코드 검색 엔진 Blackbird에서 대규모로 수행되는 유니코드 케이스 폴딩을 메모리 대역폭에 가까운 속도로 최적화했다. 핵심은 비ASCII 문자를 만나면 즉시 중단하는 기존 최적화를 제거하고, 전체 버퍼를 분기 없이 처리해 컴파일러의 SIMD 벡터화를 유도한 것이다. Apple M4에서 단순 구현의 약 3.1GiB/s 처리량을 45GiB/s 이상으로 끌어올렸으며, 분기 없는 코드는 벡터화를 가능하게 할 때만 이점이 있다는 점도 확인했다. ## 케이스 폴딩과 소문자 변환의 차이 - 케이스 폴딩은 표시를 위한 변환이 아니라 문자열 비교를 위한 정규화다. - 대소문자만 다른 문자열을 동일하게 취급하기 위해 사용된다. - 검색 엔진 - 정규식의 `(?i)` 옵션 - 대소문자를 구분하지 않는 사용자명과 호스트명 - 일반적인 소문자 변환과 달리 케이스 폴딩은: - 로케일에 독립적이다. - 문맥에 의존하지 않는다. - 비교 관계가 일관되고 대칭적이다. - 그리스어 final sigma, 터키어의 `I`, 독일어 `ß`처럼 소문자 변환과 케이스 폴딩 결과가 달라지는 문자가 있어 `to_lowercase`를 대체 수단으로 사용하면 잘못된 검색 결과가 발생할 수 있다. - 공개된 Rust `casefold` 크레이트는 Unicode `CaseFolding.txt`의 단일 문자 변환인 C·S 상태만 구현한다. - `ß → ss`와 같은 다중 문자 변환은 지원하지 않는다. - 터키어 전용 폴딩도 지원하지 않는다. - ripgrep과 정규식 엔진도 유사한 제한을 둔다. ## GitHub에서 케이스 폴딩이 중요한 이유 - Blackbird는 1억 8천만 개 이상의 저장소와 480TB가 넘는 소스 코드를 색인한다. - 색인 전에 모든 바이트를 폴딩하고, 검색 결과 후보를 확인할 때도 케이스 폴딩이 반복적으로 수행된다. - 소스 코드는 대부분 ASCII이므로 ASCII 경로를 메모리 속도로 처리하는 것이 가장 큰 성능 개선 요소다. - 비ASCII 입력은 드물기 때문에, 최적화의 목표는: - 흔한 ASCII 경로를 최대한 빠르게 만들고 - 드문 유니코드 경로가 ASCII 성능을 방해하지 않게 하는 것이다. ## 조기 종료를 제거한 ASCII 처리 기존 방식은 비ASCII 바이트를 만나면 즉시 반복문을 종료하고 유니코드 처리로 넘겼다. ```text if byte >= 0x80 { break } ``` 하지만 이 데이터 의존적 `break`는 컴파일러의 루프 벡터화를 막는다. 개선된 구현은 다음과 같이 동작한다. - 모든 바이트를 끝까지 순회한다. - `high_bit_acc |= byte`로 모든 바이트의 최상위 비트를 누적한다. - 반복문이 끝난 뒤 누적값을 한 번만 검사해 비ASCII 문자가 있었는지 확인한다. - 따라서 반복문 내부에는 데이터에 따른 분기나 조기 종료가 없다. 이 방식은 비ASCII 여부를 동일하게 알아내면서도 SIMD 명령을 사용할 수 있게 한다. ## 분기 없는 ASCII 대소문자 변환 ASCII 대문자 변환도 조건문 대신 산술 연산으로 처리한다. - `b.wrapping_sub(b'A') < 26` - 바이트가 `A`부터 `Z` 사이일 때만 참이 된다. - 별도의 분기 없이 대문자 여부를 0 또는 1 마스크로 만든다. - `u8::from(is_upper) << 5` - 대문자이면 ASCII의 비트 5를 설정해 소문자로 바꾼다. - 그 외 문자는 아무 변화가 없다. - 모든 바이트를 항상 저장하므로 조건부 저장 명령도 제거된다. 결과적으로 루프는 다음 특성을 갖는다. - 데이터 의존적 분기 없음 - 조기 종료 없음 - 연속적인 메모리 접근 - LLVM이 NEON 기반 16바이트 단위 SIMD 코드 생성 가능 ## 벤치마크와 성능 개선 Apple M4에서 5.7KB ASCII 버퍼를 처리한 누적 측정 결과는 다음과 같다. - 조기 종료와 분기 조건을 사용하는 순진한 구현: **3.1GiB/s** - 본문만 분기 없이 바꾸고 조기 종료를 유지한 구현: **2.6GiB/s** - 조기 종료를 제거한 구현: **7.6GiB/s** - 대문자 판별과 저장까지 분기 없이 처리한 최종 구현: **45GiB/s 이상** 성능 향상의 핵심은 단순히 분기를 없앤 것이 아니다. - 조기 종료 제거가 루프 벡터화를 가능하게 했다. - 대문자 변환의 분기 제거가 완전한 벡터화를 가능하게 했다. - 최종 성능은 사실상 메모리 대역폭 한계에 도달했다. ## 스칼라 코드에서는 분기 없는 방식이 느릴 수 있다 - 조기 종료를 유지한 채 본문만 분기 없이 바꾼 구현은 오히려 3.1GiB/s에서 2.6GiB/s로 느려졌다. - 기존 분기 방식은 실제로 값이 바뀌는 대문자일 때만 저장한다. - 소문자, 숫자, 공백이 대부분인 일반 텍스트에서는 조건부 저장 분기가 매우 잘 예측된다. - 반면 분기 없는 구현은 모든 바이트를 무조건 저장해 불필요한 쓰기 트래픽이 발생한다. - 따라서 분기 없는 코드는 그 자체로 항상 빠른 것이 아니다. - 이 사례에서는 분기 없는 코드가 SIMD 벡터화를 가능하게 하는 수단이었기 때문에 최종적으로 큰 이득을 냈다. 실용적으로는 데이터 규모가 크고 반복 횟수가 많은 바이트 처리에서 조기 종료가 정말 최적인지 확인해야 한다. 특히 루프의 조기 종료가 SIMD 벡터화를 막는다면, 전체 버퍼를 일정한 흐름으로 처리하고 마지막에 상태를 확인하는 방식이 훨씬 빠를 수 있다.

원문 읽기(새 탭에서 열림)
datadog원문

Arm64 JIT 컴파일러 버그를 드러낸 PostgreSQL 세그멘테이션 오류 파헤치기 (새 탭에서 열림)

Postgres 서버에서 발생한 'Segmentation fault(signal 11)' 오류의 원인을 추적하여, 이것이 Postgres 자체의 결함이 아니라 Arm64 아키텍처 환경에서 작동하는 LLVM(JIT 컴파일러)의 버그임을 밝혀낸 과정을 다루고 있습니다. 대규모 파티션 테이블을 조회할 때 JIT가 활성화되면서 크래시가 발생했으며, 이를 통해 업스트림 LLVM의 문제를 해결하는 성과를 거두었습니다. ## JIT 컴파일과 크래시의 상관관계 * **세그멘테이션 폴트 발생**: 최신 버전의 Postgres를 사용 중임에도 특정 쿼리(죽음의 쿼리)를 실행하면 서버가 즉시 종료되는 현상이 발생했습니다. * **스택 오염 확인**: 코어 덤프 분석 결과, 호출 스택(Backtrace)이 비정상적으로 짧고 깨져 있었으며, 이는 JIT 실행 함수인 `ExecRunCompiledExpr` 부근에서 문제가 발생했음을 시사했습니다. * **트리거 조건**: 64개의 파티션과 160만 개 이상의 행을 가진 대규모 테이블을 스캔할 때, Postgres의 비용 기반 휴리스틱에 의해 JIT 컴파일이 활성화되면서 오류가 유발되었습니다. ## Postgres JIT와 LLVM의 역할 * **성능 최적화**: Postgres는 반복적인 SQL 표현식 평가 오버헤드를 줄이기 위해 LLVM을 사용하여 런타임에 네이티브 머신 코드를 생성합니다. * **튜플 디포밍(Tuple Deforming)**: 디스크상의 데이터를 메모리 표현으로 변환하는 과정을 네이티브 코드로 컴파일하여 처리 효율을 극대화합니다. * **컴파일 오버헤드**: JIT는 실행 속도를 높이지만 컴파일 시간이 추가되므로, Postgres는 실행 비용이 높은 쿼리에 대해서만 선택적으로 JIT를 적용합니다. ## 문제 해결 및 근본 원인 파악 * **임시 조치**: `SET jit = off;` 설정을 통해 JIT 기능을 비활성화함으로써 쿼리 지연 시간의 큰 손해 없이 프로덕션 환경의 크래시를 즉시 중단시켰습니다. * **디버깅 결과**: 데이터 복제 및 로컬 환경 재현을 통해 분석한 결과, 특정 조건에서 LLVM이 Arm64 아키텍처용 머신 코드를 잘못 생성하는 버그가 있음을 확인했습니다. * **업스트림 기여**: 이 조사는 단순히 설정을 변경하는 것에 그치지 않고, 어셈블리 수준의 분석을 통해 LLVM 프로젝트의 코드 수정까지 이끌어내는 계기가 되었습니다. ## 권장 사항 Arm64 기반 클라우드 환경에서 Postgres를 운영 중인데 원인을 알 수 없는 Segmentation fault가 발생한다면, 우선적으로 JIT를 비활성화하여 안정성을 확보하십시오. 이후 시스템의 LLVM 라이브러리 버전을 확인하고 관련 아키텍처 패치가 적용되었는지 점검하는 것이 필요합니다.

figma3분 읽기큐레이션 요약

피그마는 웹어

WebAssembly 도입으로 Figma는 애플리케이션 초기화, 디자인 파일 다운로드, 첫 렌더링을 포함한 전체 로드 시간을 약 3배 단축했다. 특히 C++ 기반 코드를 브라우저에서 실행할 때 asm.js보다 전송·파싱·네이티브 코드 변환·캐싱 측면에서 유리했다. 다만 압축 후 다운로드 크기 감소 효과는 작았고, 당시에는 브라우저별 구현 및 캐싱 지원 차이로 적용 범위가 제한됐다. ## WebAssembly의 특징과 asm.js와의 차이 - WebAssembly는 브라우저 실행을 위해 설계된 바이너리 형식의 머신 코드다. - 기존에는 C++ 코드를 asm.js라는 JavaScript 부분집합으로 변환해 실행했다. - 숫자와 포인터만 사용할 수 있으며, 포인터는 숫자 배열의 인덱스로 표현된다. - DOM 조작이나 네트워크 연결 등 브라우저 기능은 JavaScript를 호출해야 한다. - WebAssembly도 asm.js와 동일한 제약과 브라우저 샌드박스를 공유하지만, 실행 형식이 더 효율적이다. - C++처럼 이미 LLVM으로 최적화된 코드는 브라우저가 별도 최적화를 많이 수행하지 않고 네이티브 코드로 변환할 수 있다. ## WebAssembly가 빠른 이유 - **작은 바이너리 형식** - 동일한 코드의 JavaScript보다 네트워크 전송에 유리하다. - 다만 압축하면 asm.js와 WebAssembly의 크기 차이는 크게 줄어든다. - **빠른 파싱** - 사람이 작성하는 JavaScript에는 문법과 중복 정보가 많다. - WebAssembly는 브라우저가 빠르게 해석하도록 설계되어 asm.js보다 약 20배 빠르게 파싱된다. - **효율적인 네이티브 코드 변환** - LLVM이 사전에 C++ 코드를 최적화하므로 브라우저의 런타임 최적화 부담이 작다. - **변환 결과 캐싱** - 브라우저가 WebAssembly 모듈의 네이티브 코드 변환 결과를 쉽게 캐시할 수 있다. - 같은 애플리케이션을 다시 열 때 변환 시간이 거의 사라질 수 있다. - **64비트 정수 지원** - WebAssembly는 64비트 정수를 직접 지원한다. - JavaScript는 정수 정밀도가 53비트로 제한되어 64비트 정수를 에뮬레이션해야 한다. ## Figma에서의 적용과 성능 개선 - Figma는 C++로 작성된 대규모 2D WebGL 렌더링 엔진을 사용한다. - 측정한 로드 시간에는 다음 과정이 모두 포함됐다. - 애플리케이션 초기화 - 디자인 파일 다운로드 - 디자인의 최초 전체 렌더링 - WebAssembly로 전환한 뒤 문서 크기와 관계없이 로드 시간이 3배 이상 개선됐다. - 큰 디자인 문서를 자주 만들고 전환하는 Figma 사용자에게 특히 큰 효과가 있었다. - 앱을 한 번 실행한 뒤에는 WebAssembly의 네이티브 변환 결과가 캐시되므로, 이후 로드 시간은 애플리케이션 코드 크기에 덜 영향을 받는다. ## 다운로드 크기와 실제 개선의 차이 - WebAssembly로 전환하면 전송 크기도 크게 줄어들 것으로 예상했지만 실제 감소 폭은 작았다. - asm.js 코드도 압축하면 WebAssembly와 비슷한 크기까지 줄어들기 때문이다. - 따라서 Figma에서 가장 큰 이점은 파일 크기 감소가 아니라 파싱, 코드 변환, 캐싱에 따른 실행 및 로드 시간 단축이었다. ## 브라우저 지원의 한계 - 당시 WebAssembly는 Firefox와 Chrome에서 기본 활성화되어 있었다. - Edge와 Safari는 아직 구현 중이어서 브라우저별 지원 상황을 확인해야 했다. - Figma는 Chrome에서도 WebAssembly를 사용할 수 있었지만, 구현상의 문제 때문에 실제로는 Firefox에서만 활성화했다. - 특히 Chrome의 네이티브 변환 코드 캐싱 방식이 Firefox와 달라 페이지를 열 때마다 애플리케이션 전체를 다시 변환해야 하는 문제가 있었다. WebAssembly는 대규모 C/C++ 코드베이스를 웹으로 옮길 때 특히 효과적이다. 단순히 다운로드 용량을 줄이는 기술이라기보다, 빠른 파싱과 네이티브 코드 변환, 실행 결과 캐싱을 통해 초기 로드 시간을 줄이는 기술로 보는 것이 적절하다.

원문 읽기(새 탭에서 열림)