1P by GN⁺ | ★ favorite | 댓글 1개
  • Sep 0.10.0은 AMD 9950X(Zen 5) 같은 AVX-512 지원 CPU 최적화로 저수준 CSV 파싱에서 21 GB/s를 달성해, 이전 약 18 GB/s보다 빨라짐
  • 성능 향상은.NET 9.0의 AVX-512 코드 생성에서 마스크 레지스터 왕복이 생기는 병목을 줄이도록 파서 구조를 바꾼 데서 나옴
  • AVX-512-to-256 파서는 512비트로 char를 로드한 뒤 256비트 바이트 벡터로 변환해, 마스크 처리와 별도 순열 비용을 피함
  • Sep의 저수준 파싱 성능은 2023년 0.1.0의 5950X/.NET 7.0 기준 약 7 GB/s에서 0.10.0의 9950X/.NET 9.0 기준 약 21 GB/s로 약 3배 개선됨
  • 상위 벤치마크에서도 9950X 멀티스레드 Sep은 package assets 100만 행을 72.213ms, 약 8.0 GB/s로 처리했고 floats 데이터도 약 8.1 GB/s를 기록함

Sep 0.10.0의 목표와 결과

  • Sep 0.10.0은 2025년 4월 22일 릴리스됐고, AMD 9950X(Zen 5) 같은 AVX-512 지원 CPU 최적화와 9950X 벤치마크를 포함함
  • 저수준 CSV 파싱 기준 Sep은 9950X에서 21 GB/s를 달성함
    • 0.10.0 이전에는 같은 9950X에서 약 18 GB/s였음
  • 분석 범위는 package assets CSV 데이터의 저수준 Rows 파싱이며, 모든 수치는 단일 스레드 기준임
  • 벤치마크 수치는 몇 퍼센트포인트 변동될 수 있어, 특정 릴리스에서 작은 회귀가 보일 수 있음

0.1.0부터 0.10.0까지의 성능 변화

  • Sep 성능은 코드 변경, .NET 버전 변화, CPU 세대 변화가 함께 작용하며 점진적으로 개선됨
  • 대표 성능 흐름은 다음과 같음
    • 0.1.0, 5950X, .NET 7.0: 약 7 GB/s
    • 0.3.0, 5950X, .NET 8.0: 약 12 GB/s
    • 0.6.0, 5950X, .NET 9.0: 약 13 GB/s
    • 0.9.0, 9950X, .NET 9.0: 약 18 GB/s
    • 0.10.0, 9950X, .NET 9.0: 약 21 GB/s
  • 2023년 6월 Sep 공개 이후 2년이 조금 안 되는 기간에 약 3배 빨라짐
  • 5950X에서 Sep 0.9.0을 쓴 경우와 9950X에서 Sep 0.10.0을 쓴 경우를 비교하면 약 1.6배 개선됨
    • 9950X의 부스트 클럭은 5.7GHz, 5950X는 4.9GHz임
    • 이 클럭 차이만으로 약 1.2배를 설명할 수 있다고 봄

.NET AVX-512 코드 생성의 마스크 레지스터 병목

  • Sep은 0.2.3부터 AVX-512를 지원했지만, 당시 .NET 8은 AVX-512의 k1-k8 마스크 레지스터를 명시적으로 지원하지 않았음
  • 기존 AVX-512 코드 생성에서는 비교 결과가 마스크 레지스터에 들어간 뒤 일반 레지스터로 옮겨지고, 다시 마스크 레지스터로 돌아가는 흐름이 생김
  • 9950X로 업그레이드한 뒤 Sep 0.9.0은 저수준 CSV 파싱에서 약 18 GB/s를 기록했고, 이는 5950X보다 약 1.4배 빠른 결과였음
  • 환경 변수로 파서를 바꿔 비교하자, 9950X에서 AVX2 파서가 약 20 GB/s를 기록해 기존 AVX-512 파서보다 약 10% 빨랐음
  • 이 차이로 AVX-512 마스크 레지스터 처리가 여전히 성능에 영향을 주고 있음이 확인됨

Sep 파싱 루프의 기본 구조

  • Sep의 모든 파서는 같은 기본 구조를 따르며, Parse 제네릭 메서드 하나로 따옴표 처리 여부가 다른 두 경로를 지원함
    • ParseColInfos: 따옴표 처리 시 사용하며 더 많은 상태 추적이 필요함
    • ParseColEnds: 따옴표 처리가 없을 때 사용함
  • 파싱은 배열에서 가져온 char span 단위로 수행되며, 예시는 16K 크기임
    • 이 크기는 CPU 캐시에 들어갈 만큼 작고, 이후 효율적인 멀티스레딩에도 유리함
  • 루프는 SIMD 레지스터에 16비트 문자 데이터를 로드하고, 이를 바이트 SIMD 레지스터로 변환한 뒤 CSV 특수 문자를 비교함
    • 비교 대상에는 \n, \r, ", ; 등이 포함됨
  • 비교 결과는 비트마스크로 변환되고, 마스크에서 설정된 비트만 순차적으로 파싱됨
  • 성능 차이는 이 SIMD C# 코드가 .NET에서 어떤 머신 코드로 JIT 컴파일되는지에 크게 좌우됨

기존 AVX-512 파서와 0.10.0의 조정

  • 0.9.0의 SepParserAvx512PackCmpOrMoveMaskTzcnt는 두 개의 512비트 SIMD 레지스터에 각각 32개 char를 로드하고, 이를 하나의 512비트 바이트 벡터로 패킹해 루프당 64개 문자를 처리함
  • 패킹된 데이터는 순서가 섞여 있어 PermuteVar8x64로 다시 정렬해야 함
  • .NET 9.0의 어셈블리에서는 각 Vec.Equalsvpcmpeqbvpmovm2b 두 명령으로 이어졌고, k1 같은 마스크 레지스터와 zmm 일반 벡터 레지스터 사이 이동이 반복됨
  • Sep 0.10.0에서는 MoveMask 호출을 더 앞당겨, 마스크 레지스터와 일반 레지스터 사이 왕복 횟수를 줄임
    • 다른 파서에서는 “특수 문자가 없는” 빠른 경로에서 명령 수를 줄이기 위해 필요할 때만 MoveMask를 호출함
  • 조정 후에도 마스크 레지스터에서 일반 레지스터로 이동은 남지만, 전체 어셈블리 명령 수는 감소함

AVX2와 새 AVX-512-to-256 파서

  • AVX2 기반 SepParserAvx2PackCmpOrMoveMaskTzcnt의 어셈블리는 마스크 레지스터가 없어 더 직선적인 구조를 가짐
  • 이 구조 덕분에 AVX2 파서는 기존 0.9.0 AVX-512 파서보다 빨랐음
  • 0.10.0의 새 SepParserAvx512To256CmpOrMoveMaskTzcnt는 AVX-512 명령으로 char를 로드한 뒤 ConvertToVector256ByteWithSaturation으로 256비트 바이트 벡터를 만듦
    • 실제 명령은 vpmovuswb
    • 루프당 처리량은 “단지” 32개 char지만 구조가 더 단순함
  • 이 방식은 512비트 마스크 레지스터 문제를 피하고, 패킹된 데이터가 이미 올바른 순서로 ymm4에 있어 별도 순열도 필요 없음
  • 새 파서가 9950X에서 Sep 파싱 성능을 약 21 GB/s까지 끌어올림

파서별 9950X 저수준 벤치마크

  • AMD 9950X에서 환경 변수로 모든 파서를 실행해 비교한 결과, 새 AVX-512-to-256 파서가 가장 빨랐음
  • 주요 결과는 다음과 같음
    • SepParserAvx512To256CmpOrMoveMaskTzcnt: 21597.7 MB/s, 27.0 ns/row, 1.351ms
    • SepParserVector256NrwCmpExtMsbTzcnt: 20608.5 MB/s, 28.3 ns/row, 1.416ms
    • SepParserAvx2PackCmpOrMoveMaskTzcnt: 20599.3 MB/s, 28.3 ns/row, 1.417ms
    • SepParserAvx512PackCmpOrMoveMaskTzcnt: 19944.3 MB/s, 29.3 ns/row, 1.463ms
  • Vector256 기반 크로스플랫폼 파서는 AVX2와 거의 같은 수준에 도달함
  • Vector128Vector512 기반 크로스플랫폼 파서는 여전히 빠르지만 5~10% 느렸고, Vector512Vector128보다 느렸음
  • SepParserIndexOfAny2787.0 MB/s로 크게 뒤처졌고, Vector64는 9950X에서 가속되지 않아 459.9 MB/s에 머묾

5950X와 9950X의 상위 벤치마크

  • package assets 데이터에서 100만 행 처리 결과는 9950X가 5950X보다 크게 빨랐음
    • 5950X Sep_MT: 119.430ms, 4888.1 MB/s
    • 9950X Sep_MT: 72.213ms, 8084.1 MB/s
  • 9950X에서 단일 스레드 Sep은 package assets 100만 행을 291.979ms, 1999.4 MB/s로 처리함
  • 같은 9950X package assets 벤치마크에서 비교 대상은 다음 성능을 보임
    • Sylvan: 413.265ms, 1412.6 MB/s
    • ReadLine_: 377.033ms, 1548.4 MB/s, 할당량 1991.04MB
    • CsvHelper: 1005.323ms, 580.7 MB/s
  • floats 데이터에서도 9950X의 멀티스레드 Sep은 25,000행을 2.497ms, 8136.8 MB/s로 처리함
  • 5950X에서 9950X로의 상위 벤치마크 개선 폭은 저수준 벤치마크와 비슷하게 약 1.5~1.6배

댓글과 토론

Hacker News 의견들
  • Intel이 소비자용 SKU의 다이 면적을 몇 년씩 써가며 AVX-512를 밀어붙였고, 이제 라이브러리들이 점점 쓰기 시작해 목표가 달성되는 시점에 정작 소비자용 SKU에서 빼버린 게 이상하게 느껴짐
    AMD가 AVX-512 지원을 더 잘해서 놀라운 역전이 벌어진 게 아니라, Intel이 자기 투자를 포기했기 때문에 AMD 소비자용 CPU에만 AVX-512가 남은 셈임

    • Intel은 원래 이런 식임. Optane처럼 시장을 키워놓고, Depth Cameras처럼 갑자기 발을 빼버림
      새 기술에 크게 밀어붙이다가 채택이 느리면 죽여버리는 일을 반복함. 천천히 기반을 쌓고 적절한 시점에 크게 미는 대신 그렇게 함
      Optane 지원도 Linux 커널에서 막 성숙해지고 있었을 때 접었고, 반쯤 놀고 있는 VM의 RAM 대체재로 마케팅하는 이상한 비용 절감 쪽에 집중했음
      이런 실수는 https://en.wikipedia.org/wiki/Intel_iAPX_432 때부터 계속 반복됨
    • 이 글에서 나온 속도는 원본 18 GB/s, AVX2 20 GB/s, AVX512 21 GB/s였음
      AMD CPU 기준이긴 하지만, AVX512가 AVX2 버전보다 주는 이득은 작아 보임. Intel 소비자용 칩은 E-코어에서도 AVX2를 지원함
      더 중요한 건 이게 단일 스레드 벤치마크라는 점임. Intel은 더 많은 코어를 넣기 위해 AVX512를 포기했고, 그 결과 최상위 소비자용 제품이 24코어가 됐고 AMD의 최상위 소비자용 제품은 16코어임
      실제 Intel 벤치마크를 봐야겠지만 AVX2에서 AVX512로의 개선이 작다면, 더 많은 코어에서 도는 다중 스레드 AVX2 버전이 더 적은 코어의 다중 스레드 AVX512 버전보다 빠를 가능성이 큼
      Intel E-코어는 AVX2 명령을 P-코어보다 느리게 실행하지만, 이 벤치마크에서는 어차피 AVX 이득이 작음
      Intel이 기능을 뺐다고 화내기 쉽지만, 현실적으로 AVX2만 있는 것 대비 AVX512가 주는 이득은 매우 작음. 대부분의 경우 추가 코어가 문제를 처리하는 이득으로 상쇄될 가능성이 큼
      AVX-512가 이득을 주는 아주 특정한 작업, 특히 단일 스레드 작업은 있지만, 여러 애플리케이션과 벤치마크를 섞어 보면 Intel이 나름 근거 있는 결정을 한 것 같음
    • AVX-10이 곧 나오지 않나? AVX-512의 장점 대부분을 담는 걸로 알고 있음
      사실 둘의 차이가 정확히 뭔지는 잘 모르겠음
    • Intel의 AVX-512 이야기에서 제일 이상한 점은 2015년부터 2020년까지 소비자용 CPU에 AVX-512 레지스터 파일용 다이 면적을 예약해놓고도 실제 AVX-512 기능은 넣지 않았다는 것임
      그 뒤 AVX-512가 실제로 동작하는 데스크톱 프로세서 한 세대와 노트북 프로세서 두 세대만 짧게 내고는 비활성화했음
      AMD는 이미 Intel보다 더 오래 소비자용 프로세서에 AVX-512를 넣어 출하하고 있음
    • 글에서 제일 흥미로웠던 부분은 이거였음:

      A bit surprisingly the AVX2 parser on 9950X hit ~20GB/s! That is, it was better than the AVX-512 based parser by ~10%, which is pretty significant for Sep.
      나중에 고쳤고 그게 핵심이긴 하지만, AVX-512가 소비자에게 실제로 큰 이득을 주지는 않는다는 근거도 있어 보임
      CSV를 21GB/s가 아니라 20GB/s로만 파싱할 수 있는 노트북이라도 충분히 받아들일 수 있음. 벡터 어셈블리 덕후들이 사용자보다 지원 여부에 훨씬 더 신경 쓰는 듯함

  • 각 문자마다 \n, \r, ;, "와 4번 비교하고 OR 연산 3번을 하는 대신, 흔한 트릭으로 셔플 1번, 비교 1번, OR 0번으로 처리할 수 있음
    이 트릭은 블로그에 적어둠: https://stoppels.ch/2022/11/30/io-is-no-longer-the-bottlenec... (Trick 2)
    수정: 여기서는 OR 연산 하나를 피하려고 삼항 논리를 쓰고 있어서 괜찮음. 기본적으로 (a | b | c) | d를 각각 vpternlogdvpor로 계산함

    • 고정된 문자 집합이 니블 안에 있을 때는 맞지만, 셔플에 추가 레지스터가 필요해서 반드시 더 빠르지는 않음
      여기서도 시도해봤는데 더 빠르지 않았음
  • Intel이 모든 소비자용 CPU에 느린 코어를 넣고 싶다는 이유로, 그리고 멀티 펌핑도 고려하지 않은 채 AVX-512를 전부 빼버린 데 대한 한 방임

    • 이건 상당 부분 Intel이 빠져나와야 했던 10nm 수렁에서 비롯됨
      수율이 나쁘니 비용이 높고, 그래서 다이를 최대한 줄이고 Atom 파생 코어를 넣은 뒤 에너지 절약 조치로 마케팅함
      비싼 제품군은 더 크게 만들고, 서버/클라우드 부문을 지키려고 그쪽 마진을 깎음
      실적은 망가지고 시장 점유율도 잃었지만, 적어도 시도는 한 셈임
  • Sep이 2023년 6월에 소개된 뒤 2년도 안 돼 약 3배 빨라졌다는 건 놀라운 개선이라고 했지만, 큰 하드웨어 점프를 같이 했으면 그렇게 주장하긴 어려움

    • 새 하드웨어에서 0.9.0과 0.10.0도 비교했음. 21385 대 18203이니 소프트웨어로 인한 증가는 17%
      이전 하드웨어의 0.9.0 수치인 13088에 17%를 더하면 15375이고, 0.1.0은 7335였음
      그러면 15375/7335라서, 2년도 안 된 기간의 놀라운 개선은 3배가 아니라 2.1배임
    • 같은 하드웨어에서 이전 Sep 버전 대비 3GB/s 개선이라고 주장했고, 마케팅 벤치마크와 달리 실제 달성 속도와 사용 하드웨어도 같이 적어뒀음
    • 그럴 수도 있지만, 이제는 하드웨어만으로 3배 속도 향상을 기대하던 무어의 법칙 시대는 한참 지난 듯함
      현대 기준으로는 여전히 꽤 인상적인 성과임
    • 실제로 그렇게 주장하긴 했음. 개인적으로는 이 소프트웨어를 해마다 썼을 때 얻을 수 있는 실제 성능을 보는 흥미로운 렌즈라고 봄
      물론 오해를 부르거나 논리적으로 허술한 주장일 수 없다는 뜻은 아님
    • 저 차트는 CPU 세대 4개를 통째로 건너뛰고 대규모 성능 향상처럼 보여줘서 이상함
      이 글은 바로 걸러도 될 정도임
  • 운이 좋으면 Arthur Whitney가 자극받아서 이걸 이기는 한 줄짜리 코드나 shakti 엔진 업데이트와 함께 이걸 이기는 한 줄짜리 코드를 올릴지도 모름. 발전임

  • 누가 CSV 백만 줄을 그렇게 빠르게 처리해야 하는지 생각하면 오싹함

    • 실제로 그런 적 있음. 특정 종류의 스타트업에서는 꽤 쉽게 벌어질 수 있는 상황임
      처음엔 누군가 CSV를 고름. 만들기 쉽고 데이터가 많지 않으며, 데이터를 볼 정당한 필요가 있고 Excel을 잘 아는 사람들이 더 이상 Excel 시트를 달라고 하지 않게 해주기 때문임. 사업 담당자부터 실험실 과학자까지 다양함
      내부 프로세스가 생성되는 형식인 CSV를 소비하기 시작하고, 하나 이상의 단계가 CSV를 읽는 핵심 파이프라인이 만들어짐
      그러다 고객이 생기거나 센서 처리량이 늘거나 과학 쪽이 동작하기 시작하는 등 뭔가가 잘되면서 데이터가 갑자기 10배, 100배 이상 늘어남
      그때부터 CSV 수백만 줄, 수십억 줄을 빠르게 읽는 최적화가 의미를 갖게 됨. 내부 프로세스와 다른 팀의 작업을 이 데이터에 더 적합한 형식으로 옮길 시간을 벌어줌
    • CSV는 내부에서도 매우 흔한 교환 형식이 됐고, 압축하기도 쉬움
      CSV가 거의 네트워크 카드 속도로 쏟아져 나오는 코드베이스를 다뤄본 적 있음. 원천은 Netflow였고, 이후 집계·처리된 결과가 추가 집계와 분석을 위해 CSV로 마스터에 전송됐음
      다만 왜 사람들이 그냥 protocol buffers를 쓰지 않는지는 정말 모르겠음. protobuf가 그렇게 어려운가?
    • CSV 21 GB/s를 처리한 결과를 저장한다는 게 뭘 의미하는지 생각하면 오싹함
      유용한 종류의 집계라면 좋겠지만, 구조화 데이터 검색 같은 걸 구동하는 용도라면 어딘가에는 저장해야 함
    • 여러 약점에도 불구하고 CSV는 여전히 가장 흔한 데이터 교환 형식이라고 봄
    • 금융권에는 이런 사람이 많음. 어떤 금융 회사와도 CSV를 주고받으면 처리할 수 있음. 텍스트니까
  • 어셈블리어가 나올 줄 알았는데 C# 이어서 기분 좋게 놀랐음. 아주 인상적임

  • 이 글은 21 GB/s 코드가 정확히 무엇을 하는지 명확히 정의하지 않음
    정확히 어떤 형식을 파싱하는지 궁금함. 예를 들어 CSV 방언이 따옴표로 감싼 쉼표를 지원하는지, 아니면 단순히 쉼표와 줄바꿈만 찾는 파서인지가 중요함
    파서가 결과로 무엇을 하는지도 궁금함. 예를 들어 자료구조를 채우는지 같은 부분임

    • 글에는 ns/row 계산이 있지만 rows per second로 바꾸지는 않았고, 대략 27 ns/row, 즉 초당 약 37,000행임
      그게 21GB라면 행 하나가 570k라는 뜻인데, 상당히 조작된 벤치마크처럼 보임
  • CSV의 비표준적인 성격을 생각하면 처리량을 바이트 단위로 말하는 건 의미가 없음
    JSON은 출력이 무엇이 될지, 예를 들어 부동소수점 수, 정수, 문자열, 해시맵 등을 알 수 있으니 의미가 있음
    CSV에서는 각 열마다 문자열만 얻으므로, 21 GB/s 쉼표 분리는 의미 없음의 정점임. 결국 문자열 데이터를 다시 파싱해야 하니 그래서 뭐가 달라지나 싶음
    블로그 글이 부동소수점 파싱을 언급하긴 하지만, 한 줄에 부동소수점 하나만 있어도 “CSV”로 칠 수 있음
    README.MD를 읽으라고 반박할 수도 있겠지만, 그러면 의심이 사실로 드러남. 기본값으로 이스케이프나 따옴표 처리를 실제로 하지 않아서, 인용된 수치는 매우 오해를 부르는 광고에 가까움

    • CSV는 RFC 4180으로 표준화되어 있음. 우리가 인터넷 “표준”이라고 부르는 것들 대부분만큼은 표준화된 셈임
      그 외에는 동의함. 이스케이프, 즉 CSV에서 같은 의미인 따옴표 처리를 하지 않으면 제대로 구현한 게 아님
      예를 들어 RFC 4180에서는 줄바꿈을 따옴표로 감싸면 그 줄바꿈은 따옴표 문자열 안에 들어감. 하지만 그걸 처리할 필요가 없다면 CSV 파싱을 훨씬 빠르게 구현할 수 있음
      따옴표 문자열 안의 줄바꿈을 제대로 처리하려면 많은 코어를 쓰려는 경우 2패스 접근이 필요하지만, 아예 처리하지 않으면 1패스로 가능함
      이 세부사항은 https://liuliu.me/eyes/loading-csv-file-at-the-speed-limit-o...에서 다뤘음
  • 부동소수점이나 다른 데이터를 저장·교환하는 데는 CSV보다 훨씬 좋은 대안들이 있음
    HDF5 형식은 매우 좋고, 파일 안에 훨씬 더 많은 구조를 담을 수 있으며, 메타데이터와 여러 종류의 무손실·손실 압축도 지원함

    • 공식 HDF5 라이브러리에 동시 접근 지원이 없다는 점은 큰 단점이 될 수 있음. 전체 형식을 지원하는 구현이 사실상 그것뿐이기 때문임
      이 방향으로 진행 중인 작업은 있지만 [1], 언제 반영될지는 불분명함
      [1] https://github.com/LifeboatLLC/MT-HDF5