Rust 컴파일러에 실패하는 테스트를 추가하던 중, 13중 참조와 14중 참조에서 `to_string()` 코드 생성이 달라지는 이유를 추적한다.
Rust 컴파일러에 실패하는 테스트를 추가하던 중, 특이한 코드 생성 테스트를 발견했다:
rustpub fn thirteen_ref(input: &&&&&&&&&&&&&str) -> String { // CHECK-NOT: {{(call|invoke)}}{{.*}}@{{.*}}core{{.*}}fmt{{.*}} input.to_string() } // This is a known performance cliff because of the macro-generated // specialized impl. If this test suddenly starts failing, // consider removing the `to_string_str!` macro in `alloc/str/string.rs`. // pub fn fourteen_ref(input: &&&&&&&&&&&&&&str) -> String { // CHECK: {{(call|invoke)}}{{.*}}@{{.*}}core{{.*}}fmt{{.*}} input.to_string() }
테스트가 어디 있냐고 궁금할 수 있는데, CHECK와 CHECK-NOT 주석이 사실 테스트 단언(assertion)이며, LLVM의 FileCheck 프레임워크를 사용해 검증된다.
godbolt에서 열어 보면, 전자는 새 문자열을 할당하고 memcpy를 호출하는 반면, 후자는 테스트가 암시하듯 <str as core::fmt::Display>::fmt를 호출하는데, 이는 더 비효율적이다. 그런데 왜 하필 14일까?
to_string_str 매크로는 해당 주석이 작성된 이후 위치가 바뀌었고, 지금은 library/alloc/src/string.rs에 있다:
rustmacro_rules! to_string_str { {$($type:ty,)*} => { $( impl SpecToString for $type { #[inline] fn spec_to_string(&self) -> String { let s: &str = self; String::from(s) } } )* }; } to_string_str! { Cow<'_, str>, String, &&&&&&&&&&&&str, &&&&&&&&&&&str, &&&&&&&&&&str, &&&&&&&&&str, &&&&&&&&str, &&&&&&&str, &&&&&&str, &&&&&str, &&&&str, &&&str, &&str, &str, str, }
특별한 제네릭도 없고, 비밀스러운 컴파일러 내부 주술도 없고, 미친 타입 시스템 마법도 없다. 원하는 걸 얻을 때까지 그냥 평범하게 복사-붙여넣기 했을 뿐이다. 나는 이게 정말 좋다.
그럼 왜 14일까?
아마도 Rust는 13개의 참조면 누구에게나 충분해야 한다고 생각하기 때문인 듯하다.