3P by GN⁺ | ★ favorite | 댓글 1개
  • 협업 편집에서 CRDT가 느리다는 평가는 종종 알고리듬 자체구현 방식을 섞어 판단한 결과이며, 자료구조와 메모리 배치만으로 성능이 크게 달라질 수 있음
  • Automerge v1.0.0-preview2는 260,000개 편집 추적 처리에 291초와 880MB RAM을 썼고, 같은 작업을 Diamond types native는 56ms와 1.1MB RAM으로 처리함
  • Yjs는 트리 대신 평탄 리스트, 위치 캐시, 양방향 연결 리스트, span 저장을 활용해 같은 추적을 0.97초와 3.3MB RAM으로 줄임
  • Diamond types는 Rust에서 range tree/B-tree 기반 구조를 사용해 위치 조회·삽입·삭제를 log(n) 시간에 처리하고, WebAssembly에서도 Node.js 기준 193ms를 기록함
  • 이 벤치마크는 단일 사용자 로컬 편집 재생과 RAM 사용량만 본 것이어서, 실제 선택에는 저장·로드 시간, 네트워크/디스크 크기, binary encoding, 프로토콜, presence, editor binding까지 함께 봐야 함

알고리듬과 구현 성능은 별개임

  • 한 학술 비교는 Google Docs 같은 실시간 협업 편집을 여러 CRDT와 OT 알고리듬으로 구현해 벤치마크했고, 일부 알고리듬은 단순 paste 처리에 3초 이상 걸렸음
  • 느렸던 방식은 ShareJS와 Google Wave에서 쓰던 알고리듬이었지만, 해당 구현은 1000자 paste를 1000개의 개별 operation으로 쪼개 처리했음
  • 이 사례는 동시 편집 동작구현 방식을 분리해 봐야 함을 보여줌
    • 동작은 동시 편집이 같은 영역에 들어왔을 때 어떤 순서와 규칙으로 병합되는지를 뜻함
    • 구현은 프로그래밍 언어, 자료구조, 최적화 수준을 포함함
  • 같은 text OT transform 함수도 JavaScript에서는 초당 약 100,000회, C에서는 초당 20M회 실행되어 약 200배 차이가 남
  • 느린 구현 하나가 그 시스템의 모든 구현이 느리다는 증거는 아니며, 더 빠른 구현이 가능함

CRDT와 Automerge의 기본 모델

  • CRDT는 여러 사용자가 같은 데이터를 동시에 편집하게 해주며, 로컬에서 지연 없이 작업하고 나중에 동기화해 eventual consistency에 도달할 수 있음
  • Automerge는 Martin Kleppmann이 만든 협업 편집 라이브러리이며, RGA 알고리듬에 기반함
  • Automerge와 Yjs 같은 CRDT는 공유 문서를 문자 리스트로 보고, 각 문자에 고유 ID를 부여함
    • 빈 문서에 abc를 입력하면 (seph, 0), (seph, 1), (seph, 2) 같은 ID가 붙음
    • 새 문자는 “어떤 항목 뒤에 삽입되는지”를 함께 기록함
  • Automerge/RGA는 각 항목에 sequence number를 추가함
    • 새 항목은 지금까지 본 가장 큰 sequence number보다 1 큰 값을 받음
    • 자식 항목이 여러 개면 sequence number가 큰 순서로 정렬하고, 같으면 agent ID로 정렬함
  • Automerge/RGA의 동작은 세 단계로 볼 수 있음
    • 각 항목을 parent에 연결해 트리를 구성함
    • 자식이 여러 개인 항목은 sequence number와 ID로 정렬함
    • 트리를 depth-first traversal로 평탄화해 최종 리스트나 텍스트 문서를 만듦

Automerge 벤치마크와 병목

  • 벤치마크는 automerge-perf의 편집 추적을 사용함
    • Martin Kleppmann이 학술 논문을 타이핑한 내용을 문자 단위로 기록한 trace임
    • trace에는 260,000개 편집이 있고 최종 문서 크기는 약 100,000자임
    • 동시 편집은 포함되지 않음
    • 테스트는 로컬에 trace를 적용하는 시간만 측정함
    • 환경은 Ryzen 5800x 워크스테이션, Nodejs v16.1, Rust 1.52임
  • Automerge v1.0.0-preview2는 이 trace 처리에 291초가 걸렸고, 완료 시점 RAM은 880MB였음
    • 키 입력 하나당 약 10KB RAM을 쓴 셈임
    • 피크 RAM은 2.6GB였음
    • 느린 spike에서는 단일 편집 처리에 1.8초가 걸림
  • JavaScript 문자열에 직접 splice하는 baseline은 같은 편집을 0.61초와 0.1MB RAM으로 처리했지만, 협업 편집에 필요한 정보는 모두 버리는 비교용 baseline임
  • Automerge가 느린 데에는 몇 가지 구현상 이유가 있음
    • 문서가 커질수록 트리 기반 자료구조가 커지고 느려짐
    • Immutablejs를 많이 사용해 V8 optimizer와 GC가 최적화하기 어려움
    • 삽입된 각 문자를 별도 항목으로 취급해 paste도 많은 항목으로 처리됨
  • Automerge 팀은 Rust 구현인 automerge-rs를 WASM으로 쓰는 대체 구현을 작업 중이었음
    • 당시 master branch 기준으로 이 테스트의 평균 성능은 빨라지지 않았지만, 메모리 사용량은 절반으로 줄고 성능 변동은 완만해짐

Yjs식 평탄 리스트 구현

  • Yjs는 Kevin Jahns가 만든 오픈소스 CRDT 구현이며, 트리 대신 모든 항목을 단일 평탄 리스트에 저장함
  • 평탄 리스트 접근은 새 항목을 parent 바로 뒤에서부터 스캔해 삽입 위치를 찾는 방식이며, list CRDT를 리스트로 구현하는 형태임
  • 실험용 reference-crdts는 Yjs의 YATA와 Automerge의 RGA를 같은 방식으로 구현함
    • insert 함수는 약간의 차이를 제외하면 Yjs와 Automerge 동작을 거의 같은 codepath로 처리함
    • fuzzer로 의미적 동일성을 검증함
  • 이 접근의 장점은 세 가지임
    • 불균형 트리 대신 flat array를 사용해 더 작고 빠름
    • 코드가 단순함
    • Yjs, Automerge, Sync9 등 여러 list CRDT에 적용할 수 있음
  • 이론적으로 같은 위치에 동시 삽입이 많으면 느려질 수 있지만, 실제 편집에서는 대부분 parent 바로 뒤에 삽입함
  • reference-crdts 구현은 Automerge보다 약 10배 빨랐고 RAM은 약 30배 적게 사용함
테스트 처리 시간 RAM 사용량
automerge v1.0.0-preview2 291s 880MB
reference-crdts Automerge/Yjs 31s 28MB
Plain string edits in JS 0.61s 0.1MB

스캔과 삽입 비용 줄이기

  • 평탄 array 구현에는 두 가지 병목이 남음
    • 삽입할 위치를 찾기 위해 문서를 스캔해야 함
    • doc.content.splice(destIdx, 0, newItem)로 array 중간에 삽입할 때 뒤쪽 항목을 이동해야 함
  • 삭제된 항목은 다른 삽입이 참조할 수 있어 array에서 제거할 수 없고, isDeleted 같은 표시를 남겨야 함
    • 현재 문서가 100,000자라도 과거 항목까지 포함해 150,000개 array item이 있을 수 있음
    • 문서 위치 50,000에 삽입하려면 삭제 항목을 건너뛰며 대략 75,000개 item을 스캔할 수 있음
  • 이런 구조에서는 항목이 n개 있었던 문서에 삽입할 때 약 n단계가 필요하고, n개 문자를 삽입하면 O(n²) 가 됨
  • Yjs는 사람이 문서를 편집하는 방식에 맞춰 마지막 편집 위치의 (index, position) 쌍을 캐시함
    • 다음 편집은 이전 편집 위치 근처일 가능성이 높아 앞뒤로 짧게 스캔함
    • 여러 사용자가 다른 위치를 편집할 때를 위해 캐시된 위치 집합을 저장함
  • Yjs는 array 대신 양방향 연결 리스트를 사용해 위치를 찾은 뒤에는 상수 시간에 삽입함
  • 또 사람이 문자를 연속으로 입력한다는 점을 이용해 hello를 5개 문자 항목이 아니라 하나의 span으로 저장함
    • ID와 parent가 순차적으로 이어질 때만 collapse할 수 있음
    • 이 데이터셋에서는 array entry 수가 180,000개에서 12,000개로 줄어 14배 감소
테스트 처리 시간 RAM 사용량
automerge v1.0.0-preview2 291s 880MB
reference-crdts Automerge/Yjs 31s 28MB
Yjs v13.5.5 0.97s 3.3MB
Plain string edits in JS 0.61s 0.1MB

Rust와 Diamond types의 range tree 접근

  • JavaScript 객체는 content, deletion flag, ID, seq, parent 등이 포인터로 흩어진 구조가 되기 쉬워 메모리 단편화와 캐시 miss 비용이 커짐
  • Rust는 메모리 배치를 직접 제어할 수 있고, WebAssembly를 통해 웹에서도 사용할 수 있음
  • Diamond types는 Rust로 작성한 CRDT 구현이며, Yjs와 거의 같지만 내부적으로 연결 리스트 대신 range tree를 사용함
  • 내부 range tree는 약간 수정한 B-tree임
    • 일반적인 BTreeMap처럼 key를 저장하는 대신, 내부 노드가 자식에 포함된 문자 수의 합을 저장함
    • 문서의 임의 위치 조회, 삽입, 삭제를 log(n) 시간에 처리함
  • 260,000개 편집 trace는 이 tree에서 leaf node 3단계 정도로 저장되어, 어떤 item도 대략 3번의 메모리 read로 찾을 수 있음
  • remote edit merge를 위해 ID로 B-tree를 찾는 작은 index도 있으며, 해당 codepath는 이 벤치마크에는 포함되지 않음
  • leaf node는 32개 entry 블록을 고정 크기 array로 촘촘히 저장함
    • 32개 bucket size는 여러 크기로 벤치마크한 결과 잘 동작했지만, 왜 최적인지는 알 수 없다고 밝힘
  • Diamond를 diamond-js로 WASM 컴파일해 Node.js에서 호출하면 같은 trace를 193ms에 처리함
    • Yjs보다 약 5배 빠름
    • JavaScript 문자열 baseline보다 약 3배 빠름
  • native Rust 실행은 benchmark에서 56ms를 기록함
    • Automerge보다 5000배 이상 빠름
    • 초당 4.6M operation을 처리함
    • 전체 260,000개 편집 처리 중 malloc 호출은 1394회였음
테스트 처리 시간 RAM 사용량
automerge v1.0.0-preview2 291s 880MB
reference-crdts Automerge/Yjs 31s 28MB
Yjs v13.5.5 0.97s 3.3MB
Plain string edits in JS 0.61s 0.1MB
Diamond WASM via Node.js 0.19s 알 수 없음
Diamond native 0.056s 1.1MB

Ropey 분리와 tradeoff

  • Diamond 구현은 문서 텍스트 내용을 CRDT item 리스트에 직접 넣지 않고 별도 자료구조에 저장함
  • 텍스트 내용에는 Rust 라이브러리 Ropey를 사용하며, Ropey도 텍스트 관리를 위해 B-tree를 구현함
  • 이 방식에는 공학적 tradeoff가 있음
    • Ropey는 텍스트 특화 byte packing을 하므로 RAM 사용량을 줄일 수 있음
    • 삽입 시 두 자료구조를 업데이트해야 해 속도가 2배 이상 느려지고 WASM bundle도 60KB에서 120KB로 커짐
    • VS Code 같은 editor와 연결하면 editor가 문서 복사본을 유지하므로 CRDT 구조 안에 문서 내용을 저장하지 않아도 될 수 있음
  • Ropey만으로 trace를 처리하면 29ms가 걸림
  • Diamond native에서 문서 content 업데이트를 끄면 23ms와 0.96MB RAM을 기록함
    • Automerge보다 약 14,000배 빠름
    • 초당 11M operation을 처리함
    • 이 결과는 유용성보다 CRDT metadata 처리 한계를 보기 위한 실험에 가까움
테스트 처리 시간 RAM 사용량 자료구조
automerge v1.0.0-preview2 291s 880MB naive tree
reference-crdts Automerge/Yjs 31s 28MB array
Yjs v13.5.5 0.97s 3.3MB linked list
Plain string edits in JS 0.61s 0.1MB 없음
Diamond WASM via Node.js 0.20s 알 수 없음 B-tree
Diamond native 0.056s 1.1MB B-tree
Ropey Rust baseline 0.029s 0.2MB 없음
Diamond native, no doc content 0.023s 0.96MB B-tree

실제 라이브러리 선택 기준

  • 문서 기반 협업 앱을 지금 만든다면 Yjs를 쓰는 쪽이 유리함
    • Yjs는 성능, 낮은 메모리 사용량, 지원 생태계가 좋음
    • Kevin Jahns는 Yjs 통합 지원을 유료로 제공하기도 함
  • Automerge 팀도 성능을 2021년 최우선 이슈로 두고 있으며, 여러 기법으로 Automerge를 빠르게 만들 계획이 있었음
  • Diamond는 매우 빠르지만 Yjs와 Automerge 수준의 기능 parity에는 아직 작업이 많이 남아 있음
    • CRDT 라이브러리에는 operation 속도 외에도 binary encoding, network protocol, non-list 자료구조, presence, editor binding 등이 필요함
  • 데이터베이스 semantics가 필요하면 CRDT 위에서 잘 된 구현을 알지 못하며, OT 기반 ShareDB를 쓸 수 있음
  • Redwood는 P2P editing을 지원하고 full CRDT support를 계획 중인 프로젝트임

측정 방식의 제약

  • 이 벤치마크는 로컬 편집 trace 재생 시간과 RAM 사용량만 측정함
  • 로컬 사용자 입력은 충분히 빠르면 되며, CRDT가 단일 local edit를 약 1ms 이하로 처리하면 더 빠른 속도는 크게 중요하지 않을 수 있음
    • Automerge도 불운한 GC pause를 제외하면 대체로 이 수준을 만족함
  • 실제로 더 중요한 지표는 따로 있음
    • 문서가 디스크나 네트워크에서 차지하는 byte 수
    • 저장과 로드에 걸리는 시간
    • 저장된 문서를 데이터베이스 안에서 업데이트하는 시간
  • 사용한 trace는 단일 사용자 편집만 포함해, 동시 편집이 많은 pathological case가 남아 있을 수 있음
  • 현재 Yjs나 Automerge로 데이터베이스의 단일 객체를 업데이트하려면 일반적으로 전체 문서를 RAM에 로드하고, 변경하고, 전체 문서를 다시 저장해야 해 느릴 수 있음
    • Kevin은 Yjs provider를 적절히 조정하면 합리적인 방식으로 구현할 수 있다고 말함
  • list CRDT는 삭제된 항목 tombstone 때문에 기본적으로 계속 커지며, pruning은 별도 접근임
    • Yjs의 GC 알고리듬과 Antimatter가 예시로 나옴
    • pruning은 글에서 다룬 자료구조 최적화와 직교하는 문제임

비교가 완전히 통제된 실험은 아님

  • 각 최적화 단계는 여러 변수를 동시에 바꿨기 때문에 속도 향상의 정확한 원인을 분리하지 않음
  • Automerge에서 reference-crdts로 넘어갈 때 바뀐 요소는 여러 가지임
    • 트리에서 리스트로 핵심 자료구조가 바뀜
    • Immutablejs가 제거됨
    • Automerge frontend/backend protocol과 여러 Uint8Array 구조가 사라짐
    • JavaScript 스타일이 함수형에서 imperative로 바뀜
  • reference-crdts에서 Yjs로, Yjs에서 Diamond로 넘어갈 때도 변화가 단일 원인으로 분리되지 않음
  • automerge-rs가 이 테스트에서 Automerge보다 빠르지 않았다는 점은 Diamond 성능이 Rust만의 효과는 아니라는 근거지만, 정확한 기여도는 알 수 없음
  • RGA와 YATA를 같은 구현 방식으로 비교하는 것도 “동시 병합 동작이 사실상 비슷하며, 동작을 바꿔도 구현 성능이 유지된다”는 전제에 기대고 있음
    • reference CRDT implementation에서는 Yjs와 Automerge 동작이 거의 동일한 codepath와 동일한 성능을 보임
    • conflict-heavy trace에서는 성능 차이가 있을 수 있지만 실제로는 매우 드문 경우로 봄
  • Yjs는 각 item이 언제 삭제됐는지는 저장하지 않고 삭제 여부만 저장함
    • Diamond에서 삭제 시점을 저장하면 메모리 사용량이 1.12MB에서 2.34MB로 늘고 약 5% 느려짐
    • 이 글의 모든 Diamond benchmark는 Yjs 방식에 맞춘 yjs-style branch를 사용함

벤치마크 코드와 재현 자료

댓글과 토론

Hacker News 의견들
  • 2바이트 정수를 썼다면 캐시 라인 64바이트 때문일 가능성이 큼
    32개 엔트리가 정확히 캐시 라인 하나가 되어, 각 캐시 라인이 버킷 하나 전체를 담고 비싼 주 메모리 전송을 줄였을 것임

    • Knuth가 후기 프로그램들을 벤치마크하는 방식이 좋았음
      기본적으로 뭔가를 메모리에서 몇 번 읽어야 하는지 세는 카운터를 넣는데, 비슷하게 캐시 라인을 몇 번 비워야 하는지 근사할 수 있을지 궁금함
    • 배치 크기로 벤치마크하면 메모리 계층 구조와 관련된 큰 점프가 흔히 보임
      워드 크기(64비트), 캐시 정렬 가져오기 크기(보통 위에서 말한 64바이트), OS 페이지 크기(4~16KB), L1 크기(코어당 약 80KB), L2(몇 MB 수준) 같은 경계에서 튐
  • 실제 서비스에서 CRDT를 쓰면서 경험이 아주 좋은 앱이 뭐가 있는지 궁금함
    Notion이 그중 하나였던 걸로 기억하지만, 현실적으로 Notion에서 두 사람이 같이 메모하는 건 Google Docs에 비해 거의 못 쓸 정도임

    • Thymer[1]는 모든 것에 CRDT를 씀
      작업과 계획을 위한 IDE이고, 다중 사용자 앱이며, 종단간 암호화, 오프라인 우선, 선택적 자체 호스팅을 지원하고, 전체 작업공간이 하나의 그래프라서 CRDT가 자연스러운 선택이었음
      Thymer의 모든 작업은 소수의 CRDT 변환으로 축약됨. 텍스트 이동/복사, "frontmatter" 속성 변경, 카드 드래그, 파일 업로드, 태그 추가가 모두 같은 몇 가지 CRDT 연산으로 처리됨
      초기에 라이브러리 없이 구현하느라 일이 많았지만, 애플리케이션 상태가 하나의 그래프이면 페이지 간 텍스트 이동, 백링크가 있는 페이지 링크, 트랜스클루전 같은 기능을 동기화 걱정 없이 만들 수 있어 충분히 가치가 있었음
      CRDT는 모든 클라이언트가 같은 상태로 수렴함을 보장하고, 본질적으로 추가 전용이라 특정 시점 버전 관리도 공짜로 얻음
      다만 성능 때문에 몇 가지 타협은 했음. 버전 기록은 데이터가 너무 많아 오프라인에서 제공하지 않고, 어떤 경우에는 마지막 작성자 우선 충돌 해결을 씀
      전체적으로는 특히 처음부터 CRDT를 염두에 두고 앱을 설계한다면 매우 가치 있음. 더 전통적인 AJAX 앱에 나중에 다중 사용자 기능을 덧붙이는 상황이라면 CRDT를 쓰지 않을 듯함
      CRDT의 변경은 먼저 낙관적으로 적용되고, 권위 있는 이벤트 순서가 정해지면 모든 클라이언트가 마지막 공유 상태로 되돌아간 뒤 올바른 순서로 모든 이벤트를 다시 적용해야 함
      오래 오프라인이었다면 며칠치 변경을 되돌리고 재적용해야 할 수도 있음. 사용자는 뒤에서 얼마나 많은 트리 변환이 일어나는지 모르지만, CRDT는 애플리케이션 전체 설계에 영향을 줌
      오늘날 인기 있는 대부분의 앱은 CRDT 변환이 아직 잘 이해되지 않았던 시기에 설계됐음
      [1] https://thymer.com (거의 베타 준비 완료)
    • 오늘날 Notion은 리스트 데이터의 블록 순서 같은 일부 의도 보존 연산만 제한적으로 가진 마지막 작성자 우선 시스템임
      텍스트는 마지막 작성자 우선이고, 각 블록 텍스트나 속성은 마지막 작성자 우선 레지스터임. 블록 텍스트용 새 CRDT 형식을 작업 중임
    • iCloud의 대부분 서비스는 내부적으로 CRDT를 쓰는 것으로 알고 있음
      Notes, Reminders가 포함되고 Photos도 그럴 수 있음. 백엔드 일부로 FoundationDB도 쓰인다고, 술 취한 전 Apple SRE에게 술집에서 들었음
    • Linear: https://linear.app/
      Local First Conf 발표도 참고할 만함: https://youtu.be/VLgmjzERT08
    • 좀 더 생각해보면 사람들이 바로 떠올리진 못할 예가 하나 더 있음
      어떤 형태로든 롤백이나 보정을 하는 네트워크 게임 전부가 여기에 가까움. 최선 노력 방식에 롤백을 대체 경로로 두는 형태가 널리 쓰이는 CRDT 중 사용자 경험 면에서 가장 좋은 형태일 수도 있음
      엄밀한 학술적 의미의 CRDT는 아님. 기술적으로 게임 상태가 모든 클라이언트에 완전히 복제되지 않고, 각 클라이언트는 부분 게임 상태만 받기 때문임
      또 게임 클라이언트는 낮은 지연 동기화가 필요하고, 학술적으로는 이를 "조정"이라고 볼 수도 있음. 실제로는 클라이언트가 충돌 해결이나 롤백 보정이 돌아오기 전에 입력 결과를 확률적으로 로컬에서 받아들이고 렌더링함
      까다롭게 따지는 사람도 있겠지만, 세 번째 예까지 보면 공통 주제가 보일 것임. 가장 인기 있고 사용성이 좋으며 잘 구현된 CRDT들은 실제로 학술적 규칙을 깨고 있음
      이는 지나치게 학술적인 사고 모델의 전형적인 함정임. 현실의 알고리즘과 자료형은 학술적 "규칙집"보다 훨씬 창의적인 경우가 많음. 예를 들면 Timsort가 그럼
      논문 심사가 아니라 실제 사용자를 위한 제품을 만든다면 과도한 엔지니어링/학술적 함정에 빠지지 말아야 함. 학술적 규칙을 배우되 의도적으로 깨고, 학자들끼리만 유용할 만큼 엄격하게 정의한 개념을 완벽 구현하려 하기보다 실제 가치를 더하고 쓰기 좋게 만들어야 함
  • CRDT는 강력하지만, 연산 기반이든 상태 기반이든 과거 연산이나 요소의 흔적을 남긴다는 점이 아쉬움
    압축을 해도 여전히 단점이고, 도입을 걱정하게 만드는 부분임
    그래도 이 논의 덕분에 Dropbox, Syncthing 같은 파일 기반 저장소 위에 충돌 없는 알고리즘이나 세밀한 충돌 해결 알고리즘을 구현할 가능성에는 흥미가 생김

    • 글쓴이임. 이 대화를 많이 해봤고 CRDT 쪽에서도 자주 이야기하지만, 실제로는 적어도 텍스트 편집에서는 오버헤드가 너무 작아서 현실에서 문제가 될 것 같지 않음
      내 포스트-CRDT 프로젝트인 Diamond Types는 기본적으로 시간이 지날수록 무한히 커지지만, 오버헤드는 보통 지금까지 입력된 문자당 1바이트 미만임
      저장 텍스트에 LZ4 압축을 켜면 Diamond Types로 편집한 문서는 전체 편집 기록을 저장하는데도 최종 문서 상태보다 작을 때가 많음
      기술적으로 이를 해결하는 방법은 여러 개 알고 있지만, 대부분 시스템에서 이것이 실제 문제인지 확신이 없음
      yjs를 3D 모델링 도구에 쓰던 사람이 문제를 겪었다는 얘기는 들었음. 객체를 드래그하는 동안 마우스가 픽셀 단위로 움직일 때마다 영속 편집을 만들었기 때문임
      그런 작업에는 대부분 CRDT 라이브러리가 지원하지 않는 일시적 편집을 쓰는 편이 더 현명하다고 봄
      참고로 Git도 같은 문제가 있음. 저장소는 시간이 갈수록 커지고, 현대 CRDT 라이브러리를 썼을 때보다 훨씬 빠르게 커짐. 하지만 아무도 크게 신경 쓰지 않는 듯함
      Git에서 얕은 복제가 가능하긴 하지만 거의 아무도 안 함. 원한다면 CRDT에서도 마찬가지로 할 수 있음
    • 완전한 탈중앙 시스템을 만드는 게 아니라면 CRDT가 요구하는 제약 일부를 완화할 수 있음
      예를 들어 모든 클라이언트가 X 날짜 이후 변경을 받았다고 보장할 수 있다면, 그 날짜 이전의 연산은 안전하게 버릴 수 있음
    • 전체 연산 로그와 결정적 병합은 불변 블록 저장소와 잘 맞고, 보안·성능·비용 측면의 이점도 가질 수 있음
      이 분야의 최신 연구를 활용하려고 Fireproof[1]를 만들고 있음
      불변 데이터를 콘텐츠 주소화하면 각 연산이 암호학적으로 보장되는 증명이나 차이로 귀결되는 추가 이점이 있음. 인과 일관성을 강제하고 스냅샷에 안정적인 참조를 만들 수 있음
      즉 상호작용 가능하고, 오프라인에서 동작하며, 손실 없이 병합되는 데이터베이스를 엣지나 브라우저에서 돌리면서도 과거에는 중앙 데이터베이스나 블록체인에서 기대하던 무결성을 가질 수 있음
      예를 들어 서명을 위한 PDF나 스마트 계약에 스냅샷 CID를 넣어, 참조된 상태에 대한 모호성을 없앨 수 있음
      [1] https://github.com/fireproof-storage/fireproof
    • CRDT라는 개념 자체에는 과거 연산이나 요소의 흔적을 남겨야 한다는 요구가 없음
      결국 특정 자료형이나 데이터베이스가 따르는 수학적 법칙 집합에 대해 포괄적으로 말하기보다, 구체적인 구현을 비판하는 편이 더 맞음
    • 기록을 삭제할 수 있다면 무엇이 걱정인지 궁금함
  • 2021년 글이고 Automerge의 Rust 구현도 들어간 것 같으니, 갱신된 벤치마크를 보면 흥미로울 듯함

    • 글쓴이임. Yjs도 Rust로 다시 작성된 yrs가 있고, JavaScript 버전보다 훨씬 빠름
      이 문제를 푸는 완전히 다른 새 접근도 가지고 있음
      벤치마크를 갱신하면 확실히 좋겠음. 전부 더 빨라졌음
  • 내용 상당수는 이해하기 어려웠지만, 글이 너무 잘 쓰여서 멈추지 않고 읽게 되는 드문 글임

  • 이전 관련 논의들임
    https://news.ycombinator.com/item?id=28017204 (3년 전, 댓글 151개)
    https://news.ycombinator.com/item?id=33903563 (2년 전, 댓글 22개)
    https://news.ycombinator.com/item?id=41372833 (현재 게시글)
    https://news.ycombinator.com/item?id=41373288 (현재 댓글)

  • 현재 GitHub Readme[0]를 인용하면, 그 블로그 글 이후로도 성능이 또 10~80배 향상됐다고 함
    [0] https://github.com/josephg/diamond-types

  • CRDT가 느린지 설명해줄 수 있으면 좋겠음
    이 글은 미래를 보여주는 것 같음: https://joelgustafson.com/posts/2023-05-04/merklizing-the-ke...
    이것을 보고 Y.js나 Automerge와 비교해보면 좋겠음: https://github.com/canvasxyz/okra-js

    • 글쓴이임. 주된 이유는 많은 CRDT 라이브러리가 학자들에 의해 작성됐고, 그들에게 최적화할 시간이나 기술, 관심이 부족했기 때문임
      몇 년 전 이 글을 쓴 뒤로 주요 CRDT 라이브러리는 모두 몇 자릿수 규모로 빨라졌음
  • 몇 년 전에 이 글을 우연히 봤던 기억이 있음
    정말 재미있는 글이고, 최근 몇 년 사이 가장 좋아하는 글 중 하나임

    • 제목이 아마 CRDTs go brrr였던 걸로 기억함
  • "왜 WASM이 네이티브 실행보다 4배 느린가?"에 대해, 모든 문자열 연산이 WASM 메모리로 복사됐다가 결과가 계산되면 다시 JS로 복사되어야 하기 때문이라고 생각했음
    내가 틀렸나? 맥락을 잘못 이해한 건가? 진심으로 궁금함

    • 글쓴이임. 이 글은 몇 년 전 것이지만 기억상 그 부분은 통제했음. 그래서 문제는 FFI가 아니었음
      시간을 재기 전에 전체 기록을 wasm 안으로 로드했고, wasm 컨텍스트 자체에서 실행되는 Rust로 작성한 내부 루프에서 처리했음. wasm 호출은 두 번 정도뿐이었음
      4배 느려진 원인은 FFI가 아니라, 알고리즘 코드 자체가 실제로 4배 느리게 실행된 것이었음
      지금 다시 벤치마크를 돌려보면 흥미로울 듯함. 컴파일러의 wasm 출력도 좋아졌고 wasm 런타임도 빨라졌을 것임. 벤치마크 코드는 어딘가에 아직 있을 듯함
    • 그럴듯한 원인으로 보임
      별개 영역에서 계속 걸리는 문제는 다중 스레딩 이야기가 쉽지 않고, 라이브러리와 도구 지원도 완전하지 않다는 점임
      게임 엔진이나 ffmpeg, zip 같은 유틸리티 바이너리를 브라우저에서 돌려봤는데, 이 때문에 매우 느렸음
    • 더 나은 질문은 왜 둘이 같을 거라고 기대하느냐는 것 같음
      WASM 인터프리터나 JIT를 다뤄본 적은 없지만, 번역 계층을 하나 거치는 것보다 여러 계층을 거치는 편이 더 나은 경우가 얼마나 자주 있겠음?
      고수준 코드를 WASM이나 어떤 어셈블리 언어로 번역하면 고수준 코드에 담긴 많은 의도를 잃게 됨
      저수준 코드에서는 목표를 달성하기 위한 언어별 관용구의 나열을 자주 보는데, 실제 기계에 직접 대응되는 것이 있을 수도 있고 없을 수도 있음
      현대 x86-64에는 WASM에서 할 수 있는 것보다 훨씬 강력한 명령어가 아주 많음
      물론 역컴파일러도 있고, WASM JIT가 비교적 단순한 패턴 매칭으로 좋은 네이티브 코드를 만들 수 있는 매크로 연산 융합 목록이 있을 수도 있음. 아마 그렇진 않을 것이고, 교차 플랫폼 최적화는 더 어렵겠지만
      LLVM도 완벽하지 않아서, 후처리 최적화기가 개선할 수 있는 쉬운 부분은 분명 있음. 따라서 LLVM의 네이티브 출력보다 WASM을 더 빠르게 만드는 것이 이론적으로 불가능하진 않음
      하지만 아주 잘 짜인 계획이 없거나, 사실상 대상 명령어 집합 아키텍처가 지원하는 것의 상위집합인 명령어 집합을 만들지 않는 한 같은 수준의 결과를 얻기는 매우 어렵다고 봄
      내가 보기에는 WASM은 부분집합에 가까우므로, 연산을 표준화하고 실시간으로 다시 합치는 일은 쉽지 않음. 완전히 불가능하진 않아도 상당한 공학적 성취가 필요함
      직관적으로, 영어로 쓴 책을 아주 다른 언어이면서 몇천 단어로 제한된 언어로 번역한 뒤 다시 영어로 번역하면 정확히 같은 텍스트가 나오지 않음
      영어라면 한 단어로 끝날 개념을 문단으로 설명해야 하는 경우가 생김. 원래 영어를 되찾으려면 모든 것에 대한 1:1 번역이나, 두 번역자가 합의한 문단→한 단어 번역 목록이 필요함