[Rust] 성능 최적화
Rust는 성능과 관계된 매우 많은 옵션들이 제공된다.
기본적으로 안전성을 보장하기 위해 성능에서 손해를 보는 부분들이 꽤 존재하는데, 이런것 때문에 C/C++에 비해서 좀 느리게 나오기도 한다.
하지만 옵션을 조절하고 코드를 좀 마개조하면 C/C++과 비슷하거나 더 나은 성능의 코드가 충분히 나올 수 있다.
그런 방법들에 대해 몇가지 정리를 해보겠다.
release로 빌드하기
가장 기본적인 최적화 조건이다.
그냥 실행하면 일부러 빠르게 빌드하려고 대충 최적화를 하는 것도 있고, 디버그 정보를 더 집어넣기 때문에 성능이 영 좋지는 않다. 제 성능을 내게 하려면 항상 release 모드로 빌드를 해야 한다.
다음과 같이 --release 옵션을 줘서 빌드하면 된다.

그럼 4.6초가 나오던게

0.5초밖에 나오지 않는다. 이정도로 성능차가 심하다.

codegen 단위 줄이기
rust 컴파일러는 기본적으로 병렬로도 컴파일을 수행한다.
그래서 각각의 codegen 단위가 따로 노는 경우가 있는데, 이럴때는 각 codegen 단위 내에서는 최적화가 충분히 되지만, 전체적으로는 최적화가 덜될 수가 있다.
이럴때는 컴파일 단위를 강제로 1로 맞추면, 병렬 컴파일이 꺼지고 하나의 codegen 단위로 컴파일을 한다.
[profile.release]
codegen-units = 1
컴파일시간이 길어지는 대신, 약간의 성능 개선과 바이너리 크기 절감 효과가 있다.
보통은 그 차이가 아주 크지는 않다.
링크 타임 최적화(LTO)
링크 타임 최적화는 링크를 할때도 추가적인 최적화를 하는 것이다.
[profile.release]
codegen-units = 1
lto = true
컴파일 타임이 길어지는 대신 약간의 성능 개선과 바이너리 사이즈 절감 효과가 있다.
성능 개선 정도는 대체로 10% 정도인 것 같다.
참고로 LTO가 제대로 동작하려면 embed-bitcode 옵션이 true여야 하는데, 기본값이 true라서 따로 건들지는 않아도 된다.
panic check 제거하기
rust의 panic은 기본적으로, try-catch처럼도 사용할 수 있게끔 체크를 이것저것 한다.
그래서 catch_unwind로 받을 수도 있고 한데.. 진짜 터뜨리기만 해도 충분하다면 abort 모드로 하는게 좋다.
[profile.release]
codegen-units = 1
lto = true
panic = "abort"
이러면 panic에 대한 생성 코드가 줄어들어서 아주 약간의 성능 개선과 바이너리 사이즈 절감 효과가 있을 수 있다.
대체로는 미미한 것 같다.
디버그 정보 제거하기
release 모드로 빌드하면 알아서 디버그 심볼같은걸 다 날려줄 것 같지만, 그렇지는 않다.
맹글링같은건 안하고, 뭘 좀 남겨놓긴 하는 것 같더라.
그래서 릴리즈 모드로 빌드해도 프로파일러로 적당히 알아볼 수가 있다. C/C++에서 극단적인 최적화를 돌리면 못알아본다.
아래와 같이 strip 옵션을 사용하면, 디버그 정보를 다 날려버리게 만들 수 있다.
[profile.release]
codegen-units = 1
lto = true
panic = "abort"
strip = "debuginfo"CPU별 인스트럭션 사용
당장 내 기기에만 돌면 되고, 다른 비슷한 유형의 기기들과 바이너리가 호환될 필요가 없다면, 이 옵션을 주면 된다.
그럼 해당 타겟에서 가장 최적의 인스트럭션으로 최적화를 한다. 보통은 더 빠르게 만들어진 "최신" 인스트럭션을 사용하는 것으로 성능 향상의 기회를 얻는 것 같다.
[build]
rustflags = ["-C", "target-cpu=native"]
난 아직 이걸로 눈에 띄는 성능 향상을 경험해본 적은 없다.
target-feature 등록
SIMD 같은 일부 unstable한 기능을 사용하거나 할 경우에는, 해당 최적화를 위한 feature가 활성화되어있지 않을 수 있다.
그럴때는 rustflags로 feature를 임의로 켜줘야 한다.
이런 느낌이다.
[build]
rustflags = ["-C", "target-feature=+sse,+sse2,+sse3,+sse4.1,+sse4.2,+ssse3,+lzcnt,+popcnt,+avx"]함수 인라인하기
인라이닝 키워드를 꼬박꼬박 넣어주는 것도 괜찮은 습관이 될 수 있다.
웬만하면 컴파일러가 알아서 잘 해주긴 하겠지만, 애매한 경우에는 하지 않을 수 있다.
아래는 인라인을 할만한 상황이라면 최대한 인라이닝을 해달라는 요청이 된다. (심지어 함수가 존재하는 crate과, 그걸 사용하는 crate이 다르더라도)

unchecked 활용하기 (unsafe)
이건 흑마법이다.
rust에서 배열에 접근하거나 할때는 다 잠재적으로 bound check란걸 하는데, 이게 성능에 좋은건 아니다.
그래서 확실하게 보장할 수 있으면서, 성능이 중요하다면 unsafe 버전의 unchecked 기능을 사용하는게 방법일 수도 있다.
https://doc.rust-lang.org/std/primitive.slice.html#method.get_unchecked
참고: Cargo.toml profile.* 기본값
debug 빌드는
[profile.dev]
opt-level = 0
debug = true
split-debuginfo = '...' # Platform-specific.
debug-assertions = true
overflow-checks = true
lto = false
panic = 'unwind'
incremental = true
codegen-units = 256
rpath = false
release 빌드는
[profile.release]
opt-level = 3
debug = false
split-debuginfo = '...' # Platform-specific.
debug-assertions = false
overflow-checks = false
lto = false
panic = 'unwind'
incremental = false
codegen-units = 16
rpath = false프로필 기반 최적화 (PGO)
실제로 실행된 프로파일 데이터를 기반으로 한 최적화 기법이다.
컴파일이 매우 느리고, 데이터를 쌓는 것도 불편하지만, 최적화만큼은 확실하게 된다.
자바가 성능으로 자랑하는게 다 이런걸로 하는거다.
참조
https://blog.naver.com/sssang97/222888877448
관련 포스트
https://blog.naver.com/sssang97/223142654009
https://blog.naver.com/sssang97/222817883779
참조
https://nnethercote.github.io/perf-book/machine-code.html
https://doc.rust-lang.org/cargo/reference/profiles.html#lto
https://stackoverflow.com/questions/73628537/when-to-use-symbol-stripping-in-rust-release-builds
https://doc.rust-lang.org/rustc/codegen-options/index.html
https://stackoverflow.com/questions/65156743/what-target-features-uses-rustc-by-default