Rust 컴파일러에 실패하는 테스트를 추가하던 중, 13중 참조와 14중 참조에서 `to_string()` 코드 생성이 달라지는 이유를 추적한다.
Rust 컴파일러에 실패하는 테스트를 추가하던 중, 특이한 코드 생성 테스트를 발견했다:
pub 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에 있다:
macro_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개의 참조면 누구에게나 충분해야 한다고 생각하기 때문인 듯하다.