저는 늘 루비(Ruby)에서 예외(exception)가 다른 흐름 제어 메커니즘에 비해 느릴 것이라고 강하게 의심해 왔습니다. 어쨌든 예외는 단순한 break나 return보다 훨씬 복잡한 동작이니까요. 하지만 제 직감이 과거에 틀린 적도 있었기에, 이번 기회에 직접 실험해 보기로 했습니다.
아래 코드에서는 benchmark-ips 젬(gem)을 사용하여 예외, break, return으로 루프를 빠져나가는 세 가지 방식의 상대적 성능을 비교합니다. 웹에서 MRI 1.9로 유사한 벤치마크를 수행한 사례들을 본 적이 있었지만, 저는 MRI 2.2 환경에서 직접 확인해 보고 싶었습니다.
require 'benchmark/ips'
def exit_via_exception
5.times do
raise RuntimeError
end
rescue
end
def exit_via_break
5.times do
break
end
end
def exit_via_return
5.times do
return
end
end
Benchmark.ips do |x|
x.report("exception") { exit_via_exception }
x.report("break") { exit_via_break }
x.report("return") { exit_via_return }
end
벤치마크 결과
결과는 상당히 충격적입니다. 예외를 사용하는 함수는 break와 return을 사용하는 함수의 절반 이하 속도밖에 나지 않았습니다.
$ ruby exception_benchmark.rb
Calculating -------------------------------------
exception 50.872k i/100ms
break 125.322k i/100ms
return 124.173k i/100ms
-------------------------------------------------
exception 714.795k (± 2.7%) i/s - 3.612M
break 3.459M (± 3.1%) i/s - 17.294M
return 3.379M (± 3.0%) i/s - 16.888M
완벽한 벤치마크는 아닙니다
솔직히 말씀드리면, 보정하기 어려운 몇 가지 문제점이 있습니다. 예를 들어 예외 방식과 break 방식은 결국 메서드의 반환까지 이루어져야 하므로, 단순히 return만 실행하는 방식보다 더 많은 작업을 수행하게 됩니다. 또한 예외를 rescue(구조)하는 과정 자체가 성능 오버헤드에 영향을 주는지도 궁금하지만, rescue하지 않으면 벤치마크가 중단되어 버리는 딜레마가 있습니다.
그럼에도 불구하고 예외 방식이 다른 두 방식에 비해 너무나 크게 느리기 때문에, 결과가 완벽하지 않더라도 충분히 의미 있는 결론이라고 생각합니다.
우리가 얻은 교훈은?
예외를 흐름 제어 메커니즘으로 사용하고 있다면, 지금 당장 멈추세요! 특히 예외가 반복적으로 발생하고 잡히는 루프를 구성하고 있다면 더욱 그렇습니다.
그렇다면 이 결과가 개인적인 예외 사용 습관을 바꿀까요? 아마 아닐 겁니다. 느려지는 것이 평소의 '예외적인' 경우에 한한다면, 약간의 성능 저하는 감수할 만하니까요. :)
JRuby와 RBx에서는 어떨까?
Josh Cheek(@josh_cheek)는 이 글의 벤치마크보다 더 포괄적인 자신만의 버전을 작성하여, 여러 루비 구현체에서 실행해 보았습니다. 그의 결과는 해당 블로그에서 확인할 수 있습니다. 흥미롭게도 어떤 구현체에서든 역시 break가 최종 승자였다고 합니다. :)