우리 Rubyists는 우리의 해시를 사랑합니다. 그러나 해시에는 몇 가지 잘 알려진 결함이 있습니다. Richard가 Hashie Considered Harmful에서 지적했듯이 때때로 너무 유연할 수 있습니다. 즉, 오타가 빠르고 의도하지 않은 키를 할당하고 참조할 수 있습니다.
a = { type: "F150" }
a[:typo] # nil
몇 가지 일반적인 해시 대안
실제 구조화된 데이터를 저장하기 위해 해시를 사용하는 경우 실제로 유연성이 필요하지 않다고 결정할 수 있습니다. 그것은 당신을 곤경에 빠뜨릴 뿐입니다.
몇 가지 대안이 있습니다. 한 쌍의 XY 좌표를 저장해야 한다고 상상해 보십시오. 한 가지 접근 방식은 한 쌍의 XY 좌표를 유지하는 유일한 작업인 클래스를 정의하는 것일 수 있습니다.
class PointClass # I don't recommend ending class names with Class :)
attr_accessor :x, :y
def initialize(args)
@x = args.fetch(:x)
@y = args.fetch(:y)
end
end
point_class = PointClass.new(x: 1, y: 2)
point_class.x # 1
이 경우 데이터를 캡슐화하기만 하면 되므로 Struct를 사용하는 것이 더 간결한 선택일 수 있습니다. 다음과 같습니다.
PointStruct = Struct.new(:x, :y)
point_struct = PointStruct.new(1, 2)
point_struct.x # 1
세 번째 옵션은 OpenStruct를 사용하는 것입니다. OpenStruct는 일종의 구조체처럼 보이지만 해시와 같은 임의의 값을 설정할 수 있습니다. 다음은 예입니다.
point_os = OpenStruct.new(x: 1, y: 2)
point_os.x # 1
성능 영향
[2015년 7월 10일 업데이트:내 벤치마킹 스크립트가 해시에 불공평한 것 같습니다. Patrick Helm이 지적했듯이 저는 초기화에 비효율적인 방법을 사용하고 있었습니다. 따라서 해시에 대한 결과는 무시하십시오. openstruct가 매우 느리다는 나의 요점은 여전히 유효하지만. 여기에서 내 벤치마크 스크립트에 대한 그의 변경 사항을 볼 수 있습니다.]
이 네 가지 옵션을 보면서 성능에 미치는 영향이 무엇인지 궁금해지기 시작했습니다. 약간의 데이터만 처리하는 경우 이러한 옵션이 충분히 빠릅니다. 그러나 처리할 항목이 수천 또는 수백만 개라면 해시 대 OpenStruct 대 구조체 대 클래스의 성능 영향이 중요해질 수 있습니다.
Honeybadge에서는 초당 수천 개의 예외가 API에 보고되므로 이와 같은 성능 영향을 이해하는 것이 항상 마음에 듭니다.
그래서 간단한 벤치마크 스크립트를 작성했습니다. 나는 좋은 표본 크기를 자동으로 파악하고 표준 편차를 보고하기 때문에 이와 같은 실험에 벤치마크-ips gem을 사용하는 것을 좋아합니다.
초기화
PointClass, PointStruct, Hash 및 OpenStruct의 초기화 시간을 벤치마킹했을 때 PointClass와 PointStruct가 확실한 승자임을 알게 되었습니다. OpenStruct보다 약 10배, 해시보다 약 2배 빠릅니다.
PointClass 및 PointStruct는 OpenStruct보다 거의 10배 빠릅니다.
이러한 결과는 의미가 있습니다. 구조체는 가장 단순하므로 가장 빠릅니다. OpenStruct는 가장 복잡하므로(Hash에 대한 래퍼임) 가장 느립니다. 그러나 속도 차이의 크기는 다소 놀랍습니다.
이 실험을 실행한 후 속도가 중요한 모든 코드에서 OpenStruct를 사용하는 것을 정말 주저합니다. 그리고 성능이 중요한 코드에서 볼 수 있는 해시를 주의 깊게 살펴보겠습니다.
읽기/쓰기
초기화와 달리 4가지 옵션은 모두 값을 설정하고 액세스할 때 거의 동일합니다.
벤치마크 읽기 및 쓰기는 Struct, 클래스, 해시 및 OpenStruct 간에 큰 차이가 없음을 보여줍니다.
벤치마킹 스크립트
자체 시스템에서 벤치마크를 실행하려면 아래 스크립트를 사용할 수 있습니다. OSX의 MRI 2.1에서 실행했습니다. 다른 루비 인터프리터의 성능이 궁금하시다면 Michael Cohen이 MRI 2.2, JRuby 등에 대한 결과로 멋진 요지를 만들었습니다.
require 'benchmark/ips'
require 'ostruct'
data = { x: 100, y: 200 }
PointStruct = Struct.new(:x, :y)
class PointClass
attr_accessor :x, :y
def initialize(args)
@x = args.fetch(:x)
@y = args.fetch(:y)
end
end
puts "\n\nINITIALIZATION =========="
Benchmark.ips do |x|
x.report("PointStruct") { PointStruct.new(100, 200) }
x.report("PointClass") { PointClass.new(data) }
x.report("Hash") { Hash.new.merge(data) }
x.report("OpenStruct") { OpenStruct.new(data) }
end
puts "\n\nREAD =========="
point_struct = PointStruct.new(100, 200)
point_class = PointClass.new(data)
point_hash = Hash.new.merge(data)
point_open_struct = OpenStruct.new(data)
Benchmark.ips do |x|
x.report("PointStruct") { point_struct.x }
x.report("PointClass") { point_class.x }
x.report("Hash") { point_hash.fetch(:x) }
x.report("OpenStruct") { point_open_struct.x }
end
puts "\n\nWRITE =========="
Benchmark.ips do |x|
x.report("PointStruct") { point_struct.x = 1 }
x.report("PointClass") { point_class.x = 1 }
x.report("Hash") { point_hash[:x] = 1 }
x.report("OpenStruct") { point_open_struct.x = 1 }
end