빌드 플래그 하나로 20배 빨라졌다
첫 메시가 살아난 다음 날 아침, 그것은 61.9 GB 사진 라이브러리를 1.2 MB/s로 방 건너편으로 보내고 있었습니다. 완료까지 14시간. 아침 식사 무렵엔 23.8 MB/s가 되어 있었고 — 정작 효과가 있던 수정은 민망할 만큼 시시한 것이었습니다.

용의자들
떠오르는 가설은 모두 프로토콜에 관한 것이었습니다. 모든 샤드가 엄격히 하나씩 기록되고 있었죠 — 보내고, 상대의 응답을 기다리고, 다음을 보내고 — 그래서 처리량이 대역폭이 아니라 왕복 지연에 묶여 있었습니다. 약 19,000장의 사진에서는 거의 모든 파일이 단일 청크이고, 샤드마다 그 왕복을 온전히 지불합니다. 둘 다 사실이고, 둘 다 고칠 가치가 있었습니다. 그러나 둘 다 주인공은 아니었습니다.
그때 단번에 사건을 깬 질문이 왔습니다: "잠깐 — 이 두 Mac이 정말 LAN으로 이야기하는 거야, 아니면 VPN을 타고 나가는 거야?" 확인하려면 원시 링크를 측정해야 했습니다. 그래서 같은 두 기기 사이로 SSH를 통해 100 MB를 흘려보냈습니다. 20 MB/s. 네트워크는 문제없었습니다. Recilic은 이미 가진 길의 6퍼센트만 쓰고 있었습니다.
멋없는 정답
개발 빌드가 Debug 빌드였습니다. 그래서 핫패스를 두 구성에서 각각 벤치마크했습니다. 각 단계에 8 MB를 통과시켰습니다:
이레이저 코딩 인코딩(자체 Swift): 3.2 MB/s → 142.9 MB/s (44×)
ChaChaPoly 봉인(CryptoKit): 958.8 → 958.5 MB/s (1.0×)
SHA-256(CryptoKit): 3260 → 3156 MB/s (1.0×)
청크 분할(자체 Swift): 1965 → 2080 MB/s (1.1×)
격차는 전부 한 곳에 있었습니다: GF(256) 인코더. 암호화는 애초에 느리지 않았습니다 — CryptoKit은 시스템 안에서 최적화된 상태로 배포되므로 내 빌드 설정이 해칠 수 없습니다. 그러나 인코더는 우리의 Swift이고, 모든 바이트를 룩업 테이블로 곱합니다. 최적화를 켜면 gfMul이 촘촘한 루프로 인라인되고, Data의 참조 카운팅 트래픽이 사라지고, whole-module optimization이 파일을 넘어 테이블을 인라인합니다. -Onone에서는 바이트마다 실제 함수 호출과 참조 카운팅을 지불합니다.
왜 빌드 플래그 하나가 44배의 값을 하는가
660억 개의 봉투에 도장을 찍어야 한다고 상상해 보세요. 인코더가 마주한 일이 거의 그것입니다: 61.9 GB는 660억 바이트이고, 그 전부를 네 번씩 곱합니다 — 합쳐서 수천억 번의 미세한 연산. 연산 자체는 시시합니다: 표를 두 번 찾아 결합할 뿐. 당신의 오후를 결정하는 것은 각 연산을 둘러싼 서류 작업입니다.
Debug 모드는 이렇게 합니다: 봉투 하나마다 비품실까지 걸어가 도장을 가져오고, 돌아와 찍고, 도장을 반납하고, 장부에 한 줄 적습니다. 찍는 건 순간입니다. 걷는 게 전부입니다.
Release 모드는 도장을 손에 든 채로 두고, 장부를 버립니다.
용어로 되돌리면: 비품실 왕복이 함수 호출이고, 옵티마이저는 그것을 루프 안으로 인라인합니다; 장부가 자동 참조 카운팅(ARC)이고, 옵티마이저는 불필요함을 증명해 삭제합니다. 둘 다 프로그램의 계산 결과를 바꾸지 않습니다 — 바이트마다 얼마나 많은 의식이 붙는지만 바꿉니다.
그래서 Apple의 암호화는 무사합니다: 출하 시점부터 봉인되어 이미 "도장을 손에 든" 상태입니다. 빌드 설정은 그 안으로 손을 뻗을 수 없습니다. debug 세금을 내는 것은 내가 직접 쓴 코드뿐이고 — 바이트마다 냅니다.
그리고 숫자가 정확히 맞습니다: 3.2 MB/s 인코더에서 I/O와 프레이밍 오버헤드를 빼면 우리가 보던 1.2–2 MB/s입니다. 142.9 MB/s 인코더는 20 MB/s 네트워크를 훨씬 웃돌고 — 그래서 병목은 회선으로 옮겨갔습니다. 원래 있어야 할 자리로.
최적화를 켜고 다시 빌드: 1.2 → 23.8 MB/s. SSH로 측정한 20 MB/s를 넘어섭니다. 14시간 걸리던 백업이 약 45분으로. 병목은 Wi-Fi 링크로 — 원래 있어야 할 자리로 돌아갔습니다.
그래도 만들 가치가 있던 수정들
프로토콜 쪽 가설은 틀린 게 아니라 순위에서 밀린 것이었고 — 결국 함께 출시했습니다. 라인레이트에 도달하면 남은 지연이 실제로 중요해지기 때문입니다:
샤드 병행 기록. 청크의 여섯 샤드를 직렬이 아니라 병렬로 씁니다. 하나가 실패해도 그룹 전체를 drain해서 착지한 모든 샤드를 정리 목록에 등록합니다 — 절반만 쓰인 백업이 고아를 남겨선 안 됩니다.
커넥션 풀. 피어당 4개 채널. 병행 기록의 왕복이 한 파이프 뒤에 줄서지 않고 겹칩니다.
실사용에서 나온 두 상처. 전송 중에 노트북이 잠들면 예전에는 죽은 소켓 위에서 백업이 영원히 멈췄습니다 — 이제 TCP 킵얼라이브와 요청별 데드라인으로 1분 안에 "피어 사용 불가"로 드러나고, 다음 백업이 재연결합니다. 또 실행 직후 첫 연결은 macOS가 로컬 네트워크 권한 캐시를 재구성하는 동안 한 번 실패할 수 있었는데, 이제 스스로 재시도하므로 아무도 보지 못합니다.
덤으로 고친 버그: 전제 조건이 빠진 상태에서 "Back Up Now"를 누르면 — 아무 일도 일어나지 않았습니다, 그것도 조용히. 누를 수 있어 보이면서 침묵으로 답하는 버튼은 에러 메시지보다 나쁩니다. 이제는 무엇이 없는지 정확히 말합니다.
교훈을 적어 둔다
Recilic 0.1.0은 Mac App Store에서 파운딩 가격 $9.99. 메시는 무료 업데이트로 — 물론 라인레이트로 — 출시됩니다.