저주받은 Rust: 엉뚱한 방법으로 출력하기

코펜하겐 대학교 시험장에서 있었던 유명한 물리학자에 관한 이야기가 있습니다. 수험생은 기압계를 이용해 마천루의 높이를 측정하는 방법을 설명하라는 문제를 받았습니다. 학생은 건물 옥상에서 기압계를 끈에 매달아 늘어뜨린 뒤 끈의 길이와 기압계 자체의 높이를 합쳐 측정하겠다고 답했습니다. 기술적으로는 틀린 답이 아니었지만, 시험관들은 그 답을 달가워하지 않았습니다.
항의와 재평가 끝에 학생은 다양한 물리학 기반 풀이를 내놓았습니다. 기압계를 떨어뜨려 낙하 시간으로 건물 높이를 계산하는 방법부터, 건물 그림자와 기압계 그림자의 길이 비례를 이용해 기압계 높이로부터 건물 높이를 구하는 방법까지 다양했습니다. 심지어 기압계와 교환하는 조건으로 관리인에게 그냥 물어보면 된다는 유머러스한 답까지 제시했습니다.
전설에 따르면 그 물리학자는 1922년 노벨상을 수상한 닐스 보어였다고 합니다. 이 이야기는 기압계 문제로도 잘 알려져 있습니다.
왜 이 이야기가 흥미로울까요?
이 질문과 그에 대한 다양한 답은 중요한 교육적 효과를 갖습니다. 학습자에게 정석이 아닌 방법으로도 해답에 도달할 수 있음을 알려주고, 그런 방법들이 종종 정석 풀이보다 더 흥미롭다는 사실을 보여줍니다. 문제 자체에 대해 무언가를 드러내 보여주기 때문입니다.
관습적인 질문에 대한 비관습적인 답에서 배우는 것에는 가치가 있습니다. 이는 어느 정도 새로운 사고방식과 문제 해결 방식을 길러주며, 혁신의 핵심 요소이기도 합니다.
같은 원리를 Rust 학습에 적용하기
Rust 입문서라면 어디에서나 가장 먼저 등장하는 예제가 바로 “Hello, world!” 프로그램입니다.
fn main() {
println!("Hello, world!");
}Rust가 제대로 설치되었는지 확인하는 가장 쉬운 방법이기도 합니다.
하지만 관점을 뒤집어 재미를 더해 볼 수도 있습니다. println!을 쓰지 않고 “Hello, world!”를 출력하는 방법을 찾아보는 것입니다.
가능한 한 많은 비관습적인 풀이를 떠올려 봅시다. càng 기괴할수록 좋습니다! 아래의 각 풀이를 살펴보면서 왜 동작하는지, 그리고 그 풀이에서 무엇을 배울 수 있는지 이해해 보시기 바랍니다.
이 글은 밈으로 시작했습니다. 하지만 게시물이 큰 관심을 받자 본격적인 글로 확장하게 되었습니다.
당연히 이 중 어떤 방법도 실제 프로덕션 코드에서는 사용해서는 안 됩니다. 대신 이 엔터프라이즈급 hello world를 참고하시기 바랍니다.
풀이 1: println! 디슈가링하기
use std::io::Write;
write!(std::io::stdout().lock(), "Hello, world!");이 풀이가 흥미로운 이유는 println!이 사실 문자열 끝에 개행 문자를 덧붙여 write!를 호출하는 매크로에 불과하다는 것을 보여주기 때문입니다.
실제 코드는 훨씬 더 괴상합니다. 놀라움을 느끼고 싶다면 이 파일에서 print를 검색해 보시기 바랍니다. write! 자체도 write_fmt 호출로 디슈가링됩니다. 이는 Write 트레이트의 메서드입니다.
이 방법에는 실제 활용 사례도 있습니다. 무언가를 정말 빠르게 출력하고 싶다면 stdout을 한 번만 잠근 뒤 write!를 사용할 수 있습니다. 이렇게 하면 매번 println!을 호출할 때마다 stdout을 잠그는 오버헤드를 피할 수 있습니다. 이 트릭을 활용해 매우 빠른 yes 버전을 작성하는 방법은 이 글을 참고하시기 바랍니다.
풀이 2: 문자 단위로 순회하기
"Hello, world!".chars().for_each(|c| print!("{}", c));이 방법은 Rust의 강력한 이터레이터를 이용해 println!을 구현할 수 있음을 보여줍니다. 여기서는 문자열의 각 문자를 순회하며 하나씩 출력합니다.
chars()는 유니코드 스칼라 값에 대한 이터레이터를 반환합니다.
이터레이터에 대해 더 자세히는 여기에서 알아보실 수 있습니다.
풀이 3: Display 구현하기
struct HelloWorld;
impl std::fmt::Display for HelloWorld {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "Hello, world!")
}
}
println!("{HelloWorld}");이 풀이를 통해 Rust에서 트레이트가 어떻게 동작하는지 조금 배울 수 있습니다. Display 트레이트를 구현하는 구조체를 정의하면 print!로 출력할 수 있게 됩니다. 일반적으로 Display는 더 복잡한 타입을 출력 가능하게 만들기 위한 것이지만, 고정된 문자열에 대해 구현하는 것도 가능합니다!
풀이 4: Display가 꼭 필요할까요?
Display 대신 우리만의 트레이트를 만들어 보면 어떨까요?
trait Println {
fn println(&self);
}
impl Println for &str {
fn println(&self) {
print!("{}", self);
}
}
"Hello, world!".println();트레이트 메서드의 이름은 원하는 대로 정할 수 있다는 점을 활용할 수 있습니다. 이 예제에서는 println이라는 이름을 선택해 마치 표준 라이브러리의 일부인 것처럼 보이게 했습니다.
이는 println! 매크로를 완전히 뒤집는 발상입니다. 문자열을 인자로 넘기는 대신, 문자열 자체에 메서드를 호출하는 방식입니다!
풀이 5: println!이 있는데 panic!이 있으면 어떨까요?
panic!("Hello, world!");Rust에서 무언가를 출력하는 방법은 println!만 있는 것이 아닙니다. 여기서는 panic!을 사용합니다. panic!은 (부수 효과로) 문자열을 출력하고 즉시 프로그램을 종료합니다. 단 하나의 문자열만 출력하면 되는 경우에는 잘 동작합니다…
풀이 6: 난 클로저가 좋아요 ♥︎️
(|s: &str| print!("{}", s))("hello");Rust에서는 클로저를 정의한 직후 바로 호출할 수 있습니다. 클로저는 문자열 슬라이스를 인자로 받아 출력하는 익명 함수로 정의됩니다. 문자열 슬라이스는 클로저의 인자로 전달됩니다.
실전에서는 한 번만 쓰고 이름을 붙이고 싶지 않은 클로저를 정의할 때 유용할 수 있습니다.
풀이 7: C 스타일
extern crate libc;
use libc::{c_char, c_int};
use core::ffi::CStr;
extern "C" {
fn printf(fmt: *const c_char, ...) -> c_int;
}
fn main() {
const HI: &CStr = match CStr::from_bytes_until_nul(b"hello\n\0") {
Ok(x) => x,
Err(_) => panic!(),
};
unsafe {
printf(HI.as_ptr());
}
}Rust 표준 라이브러리를 쓰지 않고도 출력할 수 있습니다! 이 예제는 Rust에서 C 표준 라이브러리의 printf 함수를 호출하는 방법을 보여줍니다. 문자열을 전달하기 위해 로우 포인터를 사용하므로 unsafe합니다. 이를 통해 Rust에서 FFI가 어떻게 동작하는지 조금 배울 수 있습니다.
크레딧: /u/pinespear on Reddit 및 @[email protected]
풀이 8: C++ 스타일
이제 완전히 사이코패스 영역에 들어섰습니다… 그러니 여기서 멈출 이유가 없습니다. 정말 열심히 노력하면 Rust를 원하는 대로 구부려 C++처럼 보이게 만들 수도 있습니다.
use std::fmt::Display;
use std::ops::Shl;
#[allow(non_camel_case_types)]
struct cout;
#[allow(non_camel_case_types)]
struct endl;
impl Shl for cout {
type Output = cout;
fn shl(self, data: T) -> Self::Output {
print!("{}", data);
cout
}
}
impl Shl for cout {
type Output = ();
fn shl(self, _: endl) -> Self::Output {
println!();
}
}
cout << "Hello World" << endl; Shl 트레이트는 << 연산자를 구현하는 데 사용됩니다. cout 구조체는 Display를 구현하는 모든 타입에 대해 Shl을 구현하므로, 출력 가능한 모든 타입을 출력할 수 있습니다. endl 구조체는 cout에 대해 Shl을 구현하며, 마지막에 개행 문자를 출력합니다.
이 풀이의 크레딧은 Wisha Wanichwecharungruang 님에게 있습니다.
풀이 9: 어셈블리로 완전한 제어하기
이런 고수준 추상화들은 효율적인 출력에 방해가 될 뿐입니다. 우리는 CPU에 대한 제어권을 되찾아야 합니다. 답은 어셈블리입니다. 더 이상 낭비되는 사이클도, 숨겨진 명령어도 없습니다. 순수하고 타협 없는 성능만이 있을 뿐입니다.
use std::arch::asm;
const SYS_WRITE: usize = 1;
const STDOUT: usize = 1;
fn main() {
#[cfg(not(target_arch = "x86_64"))]
panic!("This only works on x86_64 machines!");
let phrase = "Hello, world!";
let bytes_written: usize;
unsafe {
asm! {
"syscall",
inout("rax") SYS_WRITE => bytes_written,
inout("rdi") STDOUT => _,
in("rsi") phrase.as_ptr(),
in("rdx") phrase.len(),
// syscall clobbers these
out("rcx") _,
out("r11") _,
}
}
assert_eq!(bytes_written, phrase.len());
}우리가 하는 일이 어셈블리 코드를 호출하는 것뿐이라면 애초에 왜 Rust를 쓰는지 궁금하시다면, 요점을 놓치고 계신 겁니다! 이건 단순히 출력에 관한 문제가 아닙니다. 자유에 관한 문제입니다! 내 CPU를 어떻게 써야 하는지 참견하지 마세요.
좋아요, x86_64 머신에서만 동작하지만 자유를 위한 작은 희생이라고 할 수 있습니다.
풀이 10: “미친 듯이 빠름”
비싼 돈 주고 산 CPU 코어를 제대로 활용하지도 않으면서 왜 샀을까요? 두려움 없는 동시성이 Rust의 약속 중 하나 아니었나요? 그 코어들을 제대로 활용해 봅시다!
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
fn main() {
let phrase = "hello world";
let phrase = Arc::new(Mutex::new(phrase.chars().collect::>()));
let mut handles = vec![];
for i in 0..phrase.lock().unwrap().len() {
let phrase = Arc::clone(&phrase);
let handle = thread::spawn(move || {
thread::sleep(Duration::from_millis(((i + 1) * 100) as u64));
print!("{}", phrase.lock().unwrap()[i]);
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
println!();
} 여기서는 각 문자를 별도의 스레드에서 출력합니다. 스레드는 루프 안에서 생성되며, 각 스레드는 문자를 출력하기 전에 일정 시간(밀리초) 동안 대기합니다. 이렇게 하면 CPU의 모든 힘을 동원해 문자열을 출력하는 셈입니다! 항상 문자가 올바른 순서대로 출력된다고 보장되지는 않지만(스케줄링은 원래 어렵습니다!), 엄청난 성능 향상을 생각하면 충분히 감수할 만한 타협입니다.
이제 여러분 차례입니다!
더 많은 풀이가 있다면 메시지로 알려 주시기 바랍니다.
또한 이 글이 마음에 드셨다면 매년 열리는 난독화 C 코드 콘테스트도 즐기실 수 있을 겁니다. 이전 수상작은 여기에서 확인해 보시기 바랍니다.
기압계 이야기에 더 흥미를 느끼셨다면, 비관습적인 사고방식으로 유명했던 또 다른 물리학자이자 노벨상 수상자인 리처드 파인만(Richard Feynman)이 쓴 책 《파인만 씨, 농담도 잘하시네!》(Surely You’re Joking, Mr. Feynman!)를 읽어 보시기를 권합니다.
우리 모두 틀에서 벗어나 생각하고, 문제에 대한 비관습적인 풀이를 고민해 보아야 합니다. 어쩌면 그것이야말로 문제 자체를 더 깊이 이해하는 열쇠일지도 모릅니다.
글을 무작위로 읽기