루프와 반복자의 성능 비교
루프를 쓸지 반복자를 쓸지 결정하려면, 둘 중 어느 구현이 더 빠른지도 알아야 합니다.
즉, 명시적인 for 루프를 사용한 search 함수 버전과, 반복자를 사용한 버전 중
어느 쪽이 더 성능이 좋은지 확인해야 합니다.
우리는 Sir Arthur Conan Doyle의 The Adventures of Sherlock Holmes 전체 내용을
하나의 String 으로 읽어 들인 뒤, 그 안에서 the 라는 단어를 찾는 벤치마크를
실행했습니다. 다음은 for 루프를 사용한 search 버전과 반복자를 사용한 버전의
벤치마크 결과입니다.
test bench_search_for ... bench: 19,620,300 ns/iter (+/- 915,700)
test bench_search_iter ... bench: 19,234,900 ns/iter (+/- 657,200)
두 구현의 성능은 매우 비슷합니다! 여기서 벤치마크 코드 자체를 설명하지는 않겠습니다. 중요한 점은 두 구현이 완전히 같은지 증명하는 것이 아니라, 성능 면에서 대략 어떤 비교가 나오는지 감을 잡는 것입니다.
더 포괄적인 벤치마크를 하려면, 서로 다른 크기의 다양한 텍스트를 contents 로
사용하고, 길이도 다른 여러 질의 문자열을 써 보고, 그 밖의 여러 변형도 실험해 봐야
합니다. 하지만 핵심은 이것입니다. 반복자는 고수준 추상화이지만, 결국은 여러분이
직접 저수준 코드를 썼을 때와 거의 같은 코드로 컴파일됩니다. 반복자는 러스트의
zero-cost abstractions 중 하나입니다. 즉, 그 추상화를 사용한다고 해서 추가적인
런타임 오버헤드가 붙지 않는다는 뜻입니다. 이는 C++의 원 설계자이자 구현자인
Bjarne Stroustrup 이 2012년 ETAPS 기조연설 “Foundations of C++” 에서
zero-overhead 를 정의한 방식과 비슷합니다.
일반적으로 C++ 구현은 zero-overhead 원칙을 따른다. 쓰지 않는 기능에는 비용을 지불하지 않는다. 그리고 더 나아가, 쓰는 기능도 손으로 더 잘 짤 수는 없다.
많은 경우 러스트의 반복자 코드는 사람이 손으로 직접 작성했을 법한 어셈블리와 같은 형태로 컴파일됩니다. 루프 전개(loop unrolling)나 배열 접근의 경계 검사 제거 같은 최적화도 적용되어, 결과 코드는 매우 효율적입니다. 이제 이 사실을 알았으니 클로저와 반복자를 안심하고 사용할 수 있습니다! 코드는 더 높은 수준의 추상화처럼 보이지만, 그 때문에 런타임 성능을 희생하지는 않습니다.
정리
클로저와 반복자는 함수형 프로그래밍 언어 아이디어에서 영감을 받은 러스트 기능입니다. 이들은 러스트가 저수준 성능을 유지하면서도 고수준 개념을 명확하게 표현할 수 있게 해 줍니다. 그리고 클로저와 반복자의 구현 방식은 런타임 성능에 영향을 주지 않습니다. 이것 역시 러스트가 zero-cost abstraction을 제공하려는 목표의 일부입니다.
이제 I/O 프로젝트의 표현력을 한층 끌어올렸으니, 다음으로는 cargo 의 몇 가지 기능을
더 살펴보면서 이 프로젝트를 세상과 공유하는 방법을 알아보겠습니다.