2P by GN⁺ | ★ favorite | 댓글 1개
  • 높은 경합 상황에서는 Mutex 구현의 차이가 크게 드러나며, Cosmopolitan Libc의 pthread_mutex_t는 Windows와 Linux 주요 구현보다 더 짧은 실행 시간과 낮은 CPU 사용량을 보임
  • Windows의 24-core Threadripper 29070WX 테스트에서 Cosmopolitan은 Microsoft SRWLOCK보다 2.75배 빠르고 CPU 자원은 18배 적게 사용함
  • Linux의 96-core Threadripper Pro 7995WX에서는 glibc보다 3배, musl libc보다 11배 빠르며 CPU 시간 격차는 더 크게 벌어짐
  • MacOS M2 Ultra에서는 Apple Libc가 근소하게 앞서고, Cosmopolitan은 ARM 환경에서 XNU의 ulock 시스템 호출에 의존하는 단순한 알고리듬을 사용함
  • 성능의 기반은 Google의 nsync 통합이며, CAS 빠른 경로, 대기자 큐, futex/ulock/WaitOnAddress(), 기아 방지와 designated waker 설계가 핵심임

경합 Mutex 벤치마크 방식

  • 테스트는 30개 스레드를 만들고, 각 스레드가 같은 전역 정수 g_chores100,000번 증가시키는 방식임
  • 각 증가 작업은 pthread_mutex_lock()pthread_mutex_unlock() 사이의 매우 작은 임계 구역에서 실행됨
  • 측정값은 마이크로초 단위이며 세 가지 시간을 구분함
    • wall time: 프로그램 실행에 걸린 실제 시간이며 스레드 생성과 join 오버헤드를 포함함
    • user time: 사용자 공간에서 소비한 CPU 시간
    • system time: 커널에서 소비한 CPU 시간
  • 여러 스레드가 병렬로 실행되므로 user time과 system time의 합은 wall time보다 커질 수 있음
  • 비경합 상황에서는 구현 간 성능 차이가 대체로 작지만, 경합 상황에서는 Mutex 설계 차이가 크게 드러남

Windows: SRWLOCK보다 빠른 Cosmopolitan

  • Windows 테스트는 24-core Threadripper 29070WX에서 수행됨
  • Mark Waterman의 MutexShootout은 높은 경합 시나리오에서 Windows의 SRWLOCK을 가장 강한 구현으로 평가했음
  • 같은 조건에서 Cosmopolitan pthread_mutex_t는 SRWLOCK보다 더 짧은 wall time과 더 낮은 CPU 사용량을 기록함
구현 wall time user time system time
Cosmopolitan pthread_mutex_t 148,940µs 328,125µs 62,500µs
Microsoft SRWLOCK 410,416µs 5,515,625µs 1,640,625µs
Microsoft CRITICAL_SECTION 949,187µs 7,937,500µs 5,078,125µs
MSVC 2022 std::mutex 991,750µs 12,156,250µs 4,031,250µs
spin lock 1,165,435µs 24,515,000µs 15,000µs
Cygwin pthread_mutex_t 9,780,803µs 1,937,000µs 6,156,000µs
  • Cosmopolitan Mutex는 Microsoft SRWLOCK보다 2.75배 빠르고, CPU 자원은 18배 적게 사용함
  • POSIX 구현을 Windows에 제공하는 Cygwin Mutex와 비교하면 65배 빠름
  • Cygwin Mutex는 이 사용 사례에서 spin lock보다도 느린 결과를 냄

Linux: wall time보다 더 큰 CPU 시간 격차

  • Linux 테스트는 96-core Threadripper Pro 7995WX에서 수행됨
구현 wall time user time system time
Cosmopolitan pthread_mutex_t 36,905µs 44,511µs 23,492µs
glibc pthread_mutex_t 101,353µs 150,706µs 2,724,851µs
spin lock 202,423µs 4,694,749µs 2,000µs
Musl libc pthread_mutex_t 411,013µs 2,167,898µs 9,926,850µs
  • Cosmopolitan Mutex는 glibc보다 3배, musl libc보다 11배 빠름
  • CPU 시간 기준으로는 glibc보다 42배, musl libc보다 178배 적게 사용함
  • 모든 스레드가 직렬화된 작업을 해야 하는 워크로드에서는 Cosmopolitan이 htop상 한 코어만 활성화된 것처럼 보일 수 있음
  • 같은 상황에서 glibc와 musl libc는 CPU 사용량을 크게 채울 수 있어, 같은 서버에서 여러 작업을 실행할 때 부담이 커짐

MacOS: Apple Libc가 근소하게 앞섬

  • MacOS 테스트는 M2 Ultra에서 수행됨
구현 wall time user time system time
Apple Libc 52,263µs 43,202µs 911,009µs
Cosmopolitan pthread_mutex_t 54,700µs 63,055µs 1,003,674µs
  • MacOS M2 ARM64에서는 Apple Libc가 Cosmopolitan Mutex보다 약간 빠름
  • Cosmopolitan의 일반 Mutex 구현은 이 플랫폼에서 잘 동작하지 않음
  • MacOS ARM에서 Cosmopolitan은 Ulrich Drepper의 Futexes Are Tricky에 기반한 더 단순한 알고리듬을 사용함
  • 이 방식은 무거운 처리를 대부분 XNU의 ulock 시스템 호출에 맡기며, 결과적으로 Apple 구현과 거의 같은 성능을 냄

성능의 기반: nsync 통합

  • Cosmopolitan Mutex 성능의 핵심은 Google의 nsync 라이브러리 통합임
  • nsync는 GitHub 스타가 371개인 라이브러리이며, Google의 Mike Burrows가 작성함
  • Cosmopolitan 통합 과정에서 다음 작업이 이루어짐
    • nsync의 Mutex unlock 함수에서 오래 발견되지 않은 버그를 찾아 수정함
    • AARCH64에서 C11 원자 연산으로 포팅해 경합 nsync Mutex를 upstream nsync보다 30% 빠르게 만듦
    • futex 같은 시스템 통합을 새로 작성해 런타임 이식성을 가능하게 함
    • POSIX 스레드 취소와 매끄럽게 동작하도록 만듦

nsync의 동작 방식

  • nsync는 잠금을 빠르게 얻기 위해 처음에 낙관적 CAS(compare and swap) 를 즉시 시도함
  • 잠금을 얻지 못하면 호출 스레드를 대기자들의 이중 연결 리스트에 추가함
    • 각 대기자는 별도 독립 캐시라인에 자기 세마포어를 가짐
    • 대기 상태에 들어간 스레드는 더 이상 주 잠금을 건드리지 않음
    • 여러 코어가 같은 캐시라인을 건드릴 때 생기는 통신 오버헤드를 줄이는 데 중요함
    • 관련 배경으로 Ulrich Drepper의 What Every Programmer Should Know About Memory가 연결됨
  • nsync는 운영체제의 futex를 사용해 스레드를 잠들게 함
    • MacOS에서는 futex가 ulock으로 불림
    • Windows에서는 WaitOnAddress()가 futex 역할을 함
    • Cosmo가 지원하는 OS 중 NetBSD만 futex가 없으며, POSIX 세마포어를 커널 공간에서 구현하고 각 세마포어마다 새 파일 디스크립터가 필요함
  • nsync는 “long wait” 개념으로 기아(starvation) 를 피함
    • 대기자가 30번 깨어났지만 매번 내부적으로 잠금 획득에 실패하면, 아직 기다리지 않은 스레드가 잠금을 얻지 못하도록 잠금에 비트를 추가함
    • 이 비트가 있으면 대기열이 어느 정도 비워질 때까지 새로 진입한 스레드의 초기 CAS가 실패함
  • 작은 임계 구역에서 경합하는 사용 사례는 designated waker 개념으로 빨라짐
    • 어떤 스레드가 깨어서 잠금을 얻으려 할 때 주 잠금에 비트가 설정됨
    • nsync에서는 unlock 함수가 다음 대기 스레드를 깨우는 책임을 가짐
    • 이 비트 덕분에 unlock 중인 스레드는 이미 깨어 있는 스레드가 있을 때 두 번째 대기자를 깨우지 않아도 됨
  • 관련 소스 코드는 cosmopolitan/third_party/nsync/mu.ccosmopolitan/libc/intrin/pthread_mutex_lock.c에 있음

실제 서비스와 검증 코드

  • Cosmo Mutex를 사용한 라이브 데모로 http://ipv4.games/ 서버를 볼 수 있음
  • 이 서비스는 2코어 GCE VM에서 실행되며, 지금까지 최대 49,131,669개 IP 규모의 봇넷 DDoS를 견뎠음
  • nsync 덕분에 SQL 쿼리를 백그라운드 스레드로 옮기고, 스레드들이 서로 메시지를 보내는 구조를 사용할 수 있었음
  • 상태 지표는 /statusz에서 확인 가능함
  • 벤치마크 코드는 gettimeofday()로 wall time을 재고, getrusage()로 user time과 system time을 측정함
  • 마지막에는 g_chores == THREADS * ITERATIONS를 확인해 모든 증가 작업이 수행됐는지 검증함

Spin lock을 볼 때의 주의점

  • 비경합 상황에서는 Mutex 구현 간 차이가 작고, 몇 줄짜리 spin lock이 더 나을 수도 있음
  • 하지만 spin lock은 정말 다른 선택지가 없을 때만 사용해야 함
  • 커널처럼 극도로 낮은 수준의 제약 때문에 더 복잡한 방식을 쓰기 어려운 곳에서는 유용함
  • nsync lock 내부 구현 세부 사항으로도 spin lock이 쓰일 수 있음
  • lock 성능을 wall time만으로 보면 spin lock이 좋아 보일 수 있으므로, getrusage()CPU 시간까지 함께 확인해야 함

댓글과 토론

Hacker News 의견들
  • 새 뮤텍스 구현과 비교는 언제나 흥미롭지만, 이 벤치마크 방식은 마음에 들지 않음. 거의 마이크로벤치마크처럼 보임
    빠른 락을 실제로 배포하는 사람들은 대개 매우 큰 다중 스레드 프로그램을 주된 성능 테스트 수단으로 삼음. 임계 구역 길이, 경쟁하는 스레드 수, 경쟁 정도가 다양해지는 복잡한 작업부하에서는 뮤텍스를 빠르거나 느리게 만드는 요인이 달라지는 듯함
    참고로 WebKit의 빠른 락을 작성했고, 락 구현용 ParkingLot 추상화를 발명했으며(Rust와 Unreal Engine에서도 사용됨), 예전에 Java용 빠른 락 연구와 논문도 했음

    • 데스크톱 앱을 만든 입장에서 덧붙이면, 수십 개 스레드가 자주 도는 앱에서는 경쟁이 심하지 않은 경우의 성능 수치가 보고 싶음
      실시간 오디오 프로그래머로서는 이미 잠겨 있지 않은 뮤텍스를 잡는 비용이 더 중요함. 우리 앱에서는 이 상황이 압도적으로 많음. 마찬가지로 N개 스레드가 경쟁할 때가 아니라, 실패할 try-lock 연산의 비용도 알고 싶음
      Cosmopolitan이 오픈소스이니 직접 측정할 수도 있겠지만 그래도 아쉬움
    • 같은 생각이 들었음. 뮤텍스는 여러 종류가 있고, 특정 작업부하에서는 어떤 것이 더 나음. DistributedMutexSharedMutex가 떠오름(https://github.com/facebook/folly/blob/main/folly/synchroniz..., https://github.com/facebook/folly/blob/main/folly/SharedMute...)
      해시맵과 마찬가지로, 단일 해시맵이 가능한 모든 작업부하에서 더 나은 경우는 드묾
    • 이 스타일의 뮤텍스는 Python 3.13의 PyMutex에도 쓰일 예정임. 3.13 이전의 PyThread_type_lock보다 PyMutex가 얼마나 빠른지 보여주는 실제 벤치마크가 있음
    • 확실히 마이크로벤치마크이고, 일반적인 성능을 대표하지는 못할 가능성이 큼. 이 페이지가 운영체제 벤치마킹 관행의 좋은 기준을 제시함. 다만 학계 쪽에 좀 더 맞춰져 있음: https://gernot-heiser.org/benchmarking-crimes.html
    • 그 특정 벤치마크는 오히려 바람직하지 않은 동작, 예를 들면 병적인 불공정성을 선호할 가능성이 큼. 최적 스케줄링은 첫 번째 스레드의 증가 연산을 전부 실행하고, 그다음 두 번째 스레드의 것을 전부 실행하는 식인데, 이렇게 하면 프로세서 간 트래픽이 최소화되기 때문임
      락 획득 실패 시 고정 시간(예: 100µs) 잠드는 뮤텍스는 거의 항상 작업을 뭉치게 만들어 이런 동작에 가까워지고 벤치마크에서 “이길” 수 있음. 하지만 실제 애플리케이션에서 경쟁이 조금이라도 있으면 그런 뮤텍스는 끔찍함
      이 뮤텍스가 나쁘다거나 pthread 뮤텍스가 좋다는 뜻은 아니고, 해당 마이크로벤치마크가 실제 애플리케이션 성능을 예측할 만한 것을 측정하지 않는다는 뜻임
  • “Cosmopolitan Mutex가 좋은 이유는 nsync라는 라이브러리를 썼기 때문”이라는 부분에서, nsync는 처음 들어봤지만 Mike Burrows는 Google의 프로덕션 뮤텍스 구현도 작성했음: https://github.com/abseil/abseil-cpp/blob/master/absl/synchr...
    그래서 왜 이 뮤텍스 구현이 벤치마크에서 빠졌는지 궁금함. 그리고 macOS에서 __ulock으로 위임한다면 libc++의 atomic 라이브러리에 있는 wait(), notify_one() 멤버 함수만 써도 더 단순하게 달성할 수 있을 듯함
    예전에 Rust의 뮤텍스 구현 개선과 관련한 큰 스레드도 있었음: https://github.com/rust-lang/rust/issues/93740#issuecomment-... 흥미로운 점은 거의 모든 인기 뮤텍스 구현의 내부 동작이 자세히 논의돼 있다는 것임

    • 내가 AV에 들어갔을 때 Mike는 이미 전설이었음. 검색 엔진이 더 빨라져야 할 때마다 그가 와서 핵심 함수 몇 개를 다시 쓰고 원래 하던 일로 돌아갔다는 전설이 있었음
      사실일 수도 있지만 직접 확인할 수는 없음. 효율성을 중시하는 극도로 똑똑한 엔지니어였음. 다만 우리는 한 서버에서 오래 운영하지는 않았음
    • Burrows는 Burrows-Wheeler 변환, Bigtable, Dapper, Chubby 등에도 관여했음
    • 그 Rust 스레드는 결국 도달하긴 하지만, 기본적으로는 Mara의 작업에 관한 것이고 그래서 2023년 1월에 나온 그녀의 책도 언급됨
      현재 Rust 뮤텍스 구현은 올해 초에 들어갔고, Linux에서는 크게 다르지 않을 수도 있지만 Windows와 Mac에서는 새 작업으로 알고 있음
      그래도 Mara가 다른 구현들의 내부를 설명한 내용은 여전히 흥미롭지만, 본인 상황에서 오래된 정보인지 확인하는 편이 좋음
    • Abseil의 뮤텍스 구현이 벤치마크에서 빠진 이유는 C가 아니라 C++ 구현이기 때문일 수도 있음. 추측임
    • Mike Burrows는 ACM 상도 받은 것 같고, 거기에 사진으로 보이는 것도 있음
      https://awards.acm.org/award-recipients/burrows_9434147
  • “아직 새 C 라이브러리라 거친 부분이 있지만, 너무 빠르게 좋아져서 프로덕션에서 쓰지 않는 것이 직업적 책임 방기처럼 보이기 시작했다”는 문장은 꽤 이상함. Cosmopolitan 프로젝트는 높이 평가하지만, 이런 과장된 우월성 주장은 보통 꽤 나쁜 위험 신호임

    • Justine의 주장은 대체로 맞는 편이라고 봄. 다만 과장과 자기 과시적인 표현을 쓰는 것이 그의 스타일, 혹은 성격인 듯함
      일부에게 거칠게 보일 수 있다는 것도 이해됨. 예전에도 llamacpp에서 그런 식으로 드라마가 생긴 적이 있음
    • Justine은 꽤 뛰어나고 창의적인 사람처럼 보이지만, 프로덕션에서 “새롭고” “거친 부분이 있는” libc를 쓰고 싶지는 않음
      프로덕션에서 가장 우선하는 것은 “엄청 빠르게 좋아지는 것”이 아니라 안정성, 예측 가능성, 신뢰성임. 물론 성능도 중요함. 더 빠른 코드는 인프라를 줄여 비용과 환경 측면에서 좋을 수 있음. 하지만 빠름은 마지막 순위임
    • 혼자 오랫동안 컴퓨터 앞에서 코딩하다 보면, 어쩌면 사회적 접촉이 부족해져서 어느 정도 오만함이 따라오는 듯함. 본인이나 작업의 중요성을 견제해 줄 장치가 없으면, 성과가 인상적이긴 해도 넓게 인정받은 것 이상으로 장대해 보일 수 있음
      예를 들면 APE는 매우 인상적인 해킹이라고 느끼지만, “이제 한 플랫폼에서만 불안전한 게 아니라 여러 플랫폼에서 동시에 불안전해질 수 있다는 건가?”라는 비판도 가능함
      기술 분야에 오래 있을수록 완전한 상호 이득은 극히 드물고, 대부분은 얻는 것과 잃는 것이 함께 있는 트레이드오프라는 걸 깨닫게 됨
    • 적어도 나에게는 농담처럼 보였음
    • 당신과 Justine의 유머 감각이 다를 수 있다는 생각은 해봤는지 궁금함. 이걸 여기 올려서 누구에게 도움이 된다고 생각하는지도 모르겠음
  • 완전히 곁가지지만, 게임 개발자로서 모든 개발자 빌드에서 디버그 작업을 많이 하는 느린 뮤텍스를 좋아하게 됨. 디버그 이름/ID를 갖고, 소유자를 추적하고, 경쟁에 소비한 시간을 프로파일러에 보고하고, 소유권 변경도 프로파일러에 보고하는 식임
    게임은 동시성을 다르게 구성하는 경향이 있고, 락을 피하는 패턴도 발전했음. 하지만 그런 패턴은 쓰기 어렵고 프로그래머가 구조를 바꿔야 함. 대부분의 코드는 “일단 여기 락을 붙이고 마일스톤을 넘겨보자”에서 시작함
    빠른 락도 예측 불가능하게 느려질 수 있고, 실시간 보장이 있었다면 망가뜨림. 평균적으로는 빠를 수 있지만 꼬리 지연은 사라지지 않음. “우리 게임이 끊긴다”를 추적하러 다시 돌아오는 사람이 되고 싶지는 않지만, 보통 내가 그 사람이 됨
    그러니 느린 락을 쓰는 편이 좋음. 프로파일러에서 커다랗게 빨간색으로 보이는 락 말임. 맞고 있는 게 보이면 리팩터링해서 없애면 됨
    어려운 요구라는 건 앎. AAA 프로덕션에서 프로파일러를 쓸 줄 아는 사람은 손가락으로 셀 수 있을 정도임. 여러 프로덕션을 봐도 늘 그랬음
    푸념은 미안하지만, 빠른 동시성 기본 요소와 알고리즘 연구는 계속되길 바람

    • 더 곁가지로, Rust로 게임을 개발하는 것이 즐거운 이유 중 하나가 이거임
      게임에서는 가능하다면 락 경쟁을 절대 원하지 않고, 많은 경우 락을 잡는 것이 불필요하다는 걸 증명할 수 있음. 예를 들어 각 프레임은 단계로 나뉘고, 어떤 공유 리소스에 대한 가변 접근은 특정 단계에서만 필요함. render() 전의 update()나 애셋 핫 리로드 같은 경우임
      스코프 스레드와 Rust의 빌림 규칙을 쓰면 아예 뮤텍스가 필요 없게 구조화할 수 있고, 나중에 코드가 바뀌어 필요해지는 순간 컴파일러가 엄격하게 오류를 낼 것이라고 확신할 수 있음
      가능하다면 프로파일러의 스파이크보다 컴파일 오류를 받는 쪽이 항상 좋음
    • 완전히 동의함. 교착 상태 감지나 내부 상태 확인 같은 디버깅 기능은 쉽게 제값을 함. 락을 성능에 영향을 줄 만큼 자주 획득하고 있다면 설계를 다시 봐야 함. 스레드 간 가변 상태 공유는 피해야 함
  • 한편으로 Cosmo/APE/redbean 계열은 정말 대단해 보이고, 관련 글의 댓글도 대체로 긍정적이며 개념 자체를 반박하는 내용도 별로 없음. 그런데 다른 한편으로는 다른 사람이 이걸 쓰고 있다는 이야기를 거의 못 들어봄
    모두가 자기 작업을 크게 공유하는 건 아니지만, 몇 년이나 지났다면 프로젝트 회고 글 몇 개 정도는 봤을 것 같음. 내가 본 Cosmo/APE/redbean 언급은 전부 Justine의 사이트에서 나온 것임
    그래서 궁금함. 숨은 함정이 있는 건가? 성과를 얻기 위해 뭔가 나쁜 짓을 하는 도구인가? 컴파일러나 런타임을 깊이 몰라서 내가 이해 못 하는 tom7식 농담이나 트롤링인가? 아니면 정말로 아직 널리 퍼지지 않은 기발한 도구들인가?

    • APE는 언제든 막힐 수 있는 교묘한 트릭으로 동작하고, OpenBSD에서는 실제로 막혔음
      크로스 플랫폼 소프트웨어를 만드는 대부분의 사람은 모든 플랫폼에서 실행되는 단일 실행 파일을 원하는 게 아니라, 지원하는 각 플랫폼에서 올바르게 동작하는 단일 코드베이스를 원함
      그런 관점에서는 Go처럼 CGO를 피하면 모든 대상에 크로스 컴파일할 수 있는 언어가 즐거움. 하지만 APE의 세 가지 방식으로 실행 가능한 마술은 정말 영리해도 영원히 동작할 것이라는 신뢰를 주지는 않고, 대부분은 실질적 이득도 별로 없음
      각 플랫폼에는 각자의 패키징과 서명 요구사항이 있으니, 플랫폼별 대상으로 따로 컴파일하는 편이 낫음
    • 개인적으로는, cosmo와 ape가 매우 영리해 보이지만 평범한 도구가 이미 잘 동작한다면 업무에 이런 종류의 영리함은 필요 없음
      예를 들어 이미 프로젝트를 다른 운영체제와 플랫폼으로 크로스 컴파일할 수 있거나, 그런 빌드 인프라가 있다면 어디서나 동작하는 단일 바이너리를 만드는 해법을 찾을 이유가 없음
      또한 APE는 여러 운영체제에서 실행되기 위해 영리한 해킹을 씀. 실행 파일 형식이 진화하면서 그 해킹이 언젠가 깨지면? 그 변화에 맞춰 APE를 고칠 시간이 아무에게도 없다면?
      반면 gcc, clang, go, rust 같은 지루한 도구들은 계속 업데이트되고 진화하는 운영체제에서도 계속 동작할 것임. 그래서 그냥 지루한 쪽에 머무르게 됨. 영리한 것을 신경 쓰지 않는 이유는 지루한 것이 내게는 그냥 잘 동작하기 때문임
    • Mozilla의 llamafile이 이걸 사용함. 모델 가중치와 실행 파일을 하나로 묶어서 cosmo/ape 플랫폼 어디서나 실행할 수 있게 하고, 상호작용을 위한 redbean HTTP 서버도 띄워줌
      통합된 가중치 없이 실행하고 파일시스템에서 가중치를 읽게 할 수도 있음. 로컬 LLM을 가장 쉽게 “받아서 바로 실행”하게 만드는 방식일 수 있음
    • Cosmopolitan은 늘 재미있는 블로그 글감이 되는 기술적 허점처럼 느껴졌음. 기발함과 설정에 대한 집착만으로 HN 같은 곳의 첫 페이지에 거의 보장될 만한 종류임
      하지만 libc처럼 기반 기술로 쓰기에는 주로 재미있는 장난감이나 작은 개인 프로젝트에 유용해 보임
      그런 맥락에서 glibc, musl, msvcrt 같은 것의 진지한 대안으로 제시될 때는 조금 이상하게 느껴짐. 아주 귀여운 해킹이지만, 내가 진지하게 의존하는 것에서 발견한다면 꽤 당황할 듯함
    • Mozilla에 Cosmopolitan libc 기반의 Llamafile 프로젝트가 있음: https://github.com/Mozilla-Ocho/llamafile
      Hugging Face에도 그 형식으로 다시 패키징한 인기 모델들을 정기적으로 올림: https://huggingface.co/models?search=llamafile
      다만 그것이 작은 모델을 빠르게 시험해 보는 것 이상의 실용성이 있는지는 별개 문제임
  • 그렇게 좋다면 왜 모든 C 라이브러리가 같은 트릭을 채택하지 않았는지 궁금함
    내 추측으로는 그 트릭들이 특정 아키텍처, 특정 CPU 모델, 특정 작업부하나 접근 패턴에서만 항상 빠를 가능성이 큼. 지원하는 모든 하드웨어에서 다양한 작업부하를 제대로 벤치마크하면 같은 이점이 나오지 않을 수 있음
    아니면 Cosmopolitan이 구현하려는 pthread API의 의미론이 미묘하게 다르고, 이 구현이 사양을 엄격히 준수하지 않는 것일 수도 있음
    여러 libc 작성자들이 운영체제 기본 요소의 최신 연구를 따라가지 못하고 있다고는 상상하기 어려움

    • 그런 프로젝트들은 특정 API 하나 말고도 우선순위가 수십 가지임. 개별 API에 집착하는 것은 제한된 시간을 쓰기에 좋은 방식이 아님. 그리고 반례로 Linux의 일반적인 libc에서 malloc과 문자열 루틴을 보면 됨
      glibc의 malloc은 그럭저럭 쓸 만하지만, 전체 속도와 확장성에서는 더 현대적인 대안에 쉽게 밀림. 단편화가 심하고 시간이 지나며 악화되며, 실제 작업부하에 큰 영향을 주는 MALLOC_ARENA_MAX 같은 조정값도 많음. musl malloc은 성능 면에서 모든 수준에서 끔찍함. 다중 스레드 프로그램에서 musl 할당자를 쓰면 성능을 심하게 망쳐서 거의 과실이라고 할 만했음
      musl에는 SIMD 최적화 문자열 비교 루틴 같은 것도 없음. 비사소한 프로그램에서 이런 작업에 쓰이는 CPU 사이클이 얼마나 많은지 알면 놀랄 것이고, 실제 프로파일에도 분명히 나타나며, 이를 개선하면 거의 모든 프로그램이 보편적으로 좋아짐. glibc의 최적화 루틴은 좋지만 여전히 더 빨라질 수 있어 보임
      이런 것들은 “한 아키텍처에만 특화돼 일반화되지 않는 최적화”가 아님. 특히 이 두 영역은 거의 모든 작업부하에서 벽시계 시간을 2~5배 줄이고, 장기 작업 집합 활용도도 크게 개선하는 잘 탐구되고 이해된 영역임. 그런데 왜 채택하지 않았나? 늘 그렇듯 다른 할 일이 있었거나, musl처럼 최고 성능보다 단순성을 우선하는 상충 우선순위가 있었기 때문일 가능성이 큼
      이런 프로젝트를 탓하는 건 아님. 아무도 “내 프로그램은 형편없이 느리고 아무것도 제대로 못 하게 설계했으며, 나는 그게 자랑스럽다”고 하지는 않음. 다만 그 프로젝트 작업자들이 완벽한 파레토 경계만 골라 설계했다는 생각은 전혀 현실적이지 않고, 대부분의 프로젝트가 실제로 굴러가는 방식을 담아내지 못함
    • 정치, NIH 증후군, 오래된 유지보수자 때문일 수 있음
      glibc나 C++ 쪽 동등한 것에서 뭔가를 바꾸려면 영원히 걸림
      동기화 기본 요소에는 여러 종류가 있는데 pthreads는 그중 일부만 지원함. 그것으로 자신을 제한하면 대체로 성능을 포기하는 대신 이식성을 얻는 셈임
    • “여러 libc 작성자들이 운영체제 기본 요소의 최신 연구를 따라가지 못하고 있다고는 상상하기 어렵다”는 게 비꼼인지 궁금함
      libc 유지보수자는 모르지만, 몇 가지를 유지보수하는 입장에서 최신 연구를 구현하려고 하지는 않음. 안정성을 유지하고 성능이 받아들일 만한지 확인하려고 함. 연구 구현은 내 “유지보수” 예산 밖임
    • pthread 뮤텍스 구현을 바꾸는 데 ABI 고려사항이 있는지 궁금함
    • “그렇게 좋다면 왜 모든 C 라이브러리가 같은 트릭을 채택하지 않았나?”라는 질문은 이런 농담이 떠오름
      한 남자와 통계학자가 길을 걷다가 50유로 지폐를 봄. 통계학자는 계속 걷고, 남자는 멈춰서 “봐요, 바닥에 돈이 있어요”라고 말함. 그러자 통계학자는 “가짜겠죠. 진짜였다면 누가 이미 주웠을 테니까요”라고 말하고 계속 걸어감. 다른 남자는 그 돈을 주워 감
  • 스레드와 뮤텍스는 컴퓨터 과학에서 가장 복잡하게 만드는 요소임. 새 구현은 수년 동안 대규모로 사용되기 전까지 항상 회의적으로 봄
    이런 스레딩 메커니즘의 버그는 가장 강도 높은 검토도 피해 가는 경우가 많음. 90년대 중반 Java가 등장했을 때 Solaris의 온갖 스레드와 뮤텍스 버그가 드러났음
    가장 빠른 뮤텍스 구현이 필요한 게 아니라 신뢰할 수 있는 구현이 필요함

    • 뮤텍스가 가장 “복잡한” 것과는 거리가 있음. 효율적으로 구현하는 방법도 그리 많지 않음. 대부분의 경우, 특히 읽기 경로에서는 피하는 편이 가장 좋음
  • 이 코드는 뮤텍스 락 성능이 아니라 뮤텍스 경쟁을 벤치마크함. 이런 식으로 락을 쓰고 있다면 코드를 다시 평가해야 함
    각 스레드가 g_chores를 증가시킬 때마다 뮤텍스를 잠그고 풂. 이 때문에 뮤텍스를 자주 획득하고 해제하는 오버헤드가 생기며, 스레드당 100,000번 반복됨
    이 오버헤드가 락 메커니즘 간 실제 성능 차이를 가림. 벤치마크가 실제 작업이 아니라 락 경쟁에 지배되기 때문임. 이런 벤치마크는 쓸모없음

  • Justine과 그 작업의 팬이지만, 이건 아마 뮤텍스 벤치마크 테스트 사례로는 가장 덜 흥미로운 쪽임. 여러 스레드가 같은 뮤텍스를 계속 두드리는 상황은 애초에 피해야 함
    그래서 어떤 뮤텍스 구현이 이 경우를 가장 잘 처리하는지는 별로 흥미롭지 않다고 봄

    • 뮤텍스의 좋은 벤치마크 테스트 사례로는 무엇을 생각하는지 궁금함
    • 내가 락이나 세마포어를 쓰는 대부분의 경우는 매우 비싼 리소스 주변임. 그 리소스의 사용량이 락의 성능 오버헤드를 압도함
    • 그럼 무엇을 측정해야 할까? 경쟁이 없는 경우는 중요하고 기준선이 되지만, 그 외에는 뮤텍스의 약점이 바로 이 지점임. 경쟁을 잘 처리하지 못하면 하드웨어가 놀거나, 스케줄러 작업이 늘거나, 커널 진입이 많아짐
      중요한 것 하나를 빼먹었는데, 경쟁 상황에서 성능이 나쁜 락은 메모리 네트워크에 핫스팟을 만드는 등 매우 부정적인 시스템적 효과를 낼 수 있고, 이것도 여기서 드러날 것임
    • “여러 스레드가 같은 뮤텍스를 계속 두드려서는 안 된다”는 평가에는 완전히 동의하기 어려움
      같은 뮤텍스에 여러 스레드가 몰리는 경우가 몇 가지 떠오름. 간단한 예로 리스트나 딕셔너리 같은 자료구조를 동시에 채우는 작업이 있음
      메시지 전달로도 할 수는 있지만, 메모리를 더 쓰고 공유 위치에 쓰기 위해 기다리는 것보다 느릴 수 있음
  • 프로덕션은 속도, 효율, 또는 명백히 “영리한 해킹”에 관한 것이 아님
    일요일 새벽 3시에 고장 난 시스템을 고치러 불려 가지 않는다는 보장을 위해 효율의 50%를 희생해야 한다면, 매번 그 선택을 할 것임
    프로덕션은 신뢰성에 관한 것이고, 신뢰할 수 있는 코드를 쓰는 것은 “빠른” 코드를 쓰는 것보다 10배 더 어려움