Gsub vs Tr
Сейчас я расскажу о бесполезной микрооптимизации, или как ускорить то, что и так быстро работает, в четыре раза.
Вот, допустим, в каком-то опенсорс проекте есть вот такой код:
def base_zip_log_name
t = Time.now.utc.iso8601
# Name the file based on GUID and time. GUID and Date/time of the request are as close to unique filename as we're going to get
%(App-#{guid}-#{t}).gsub!(/:|\./, "_")
end
guid
тут - это, скорее всего, строка вроде такой: 644e1dd7-2a7f-18fb-b8ed-ed78c3f92c2b
.
t
- что-то такое: 2005-08-09T18:31:42.201
gsub!
, очевидно, заменяет двоеточия и точки на подчёркивания.
Его и будем ускорять.
Оптимизируя gsub!
, можно использовать разные подходы. Можно упростить регулярку. Иногда можно заменить gsub!
на sub!
. Можно заменить gsub!
на gsub
. Иногда помогает замена gsub!
на tr!
, если это применимо. Иногда помогает замена регулярки на простую строку. Попробуем всё, что можно:
require 'benchmark/ips'
Benchmark.ips do |x|
t = '2005-08-09T18:31:42.201'
guid = '644e1dd7-2a7f-18fb-b8ed-ed78c3f92c2b'
x.report("*gsub!(/:|\./, '_') ") { %(App-#{guid}-#{t}).gsub!(/:|\./, "_") }
x.report("gsub!(/[:.]/, '_') ") { %(App-#{guid}-#{t}).gsub!(/[:.]/, '_') }
x.report("gsub(/:|\./, '_') ") { %(App-#{guid}-#{t}).gsub(/:|\./, "_") }
x.report("gsub(/[:.]/, '_') ") { %(App-#{guid}-#{t}).gsub(/[:.]/, '_') }
x.report("gsub!('.', '_').gsub!(':', '_')") { %(App-#{guid}-#{t}).gsub!('.', '_').gsub!(':', '_') }
x.report("gsub('.', '_').gsub(':', '_') ") { %(App-#{guid}-#{t}).gsub('.', '_').gsub(':', '_') }
x.report("tr!(':.', '__') ") { %(App-#{guid}-#{t}).tr!(':.', '__') }
x.report("tr(':.', '__') ") { %(App-#{guid}-#{t}).tr(':.', '__') }
x.report("tr!(':.', '_') ") { %(App-#{guid}-#{t}).tr!(':.', '_') }
x.report("tr(':.', '_') ") { %(App-#{guid}-#{t}).tr(':.', '_') }
x.report("tr!(':', '_').tr!('.', '_') ") { %(App-#{guid}-#{t}).tr!(':', '_').tr!('.', '_') }
x.report("tr(':', '_').tr('.', '_') ") { %(App-#{guid}-#{t}).tr(':', '_').tr('.', '_') }
x.compare!
end
И вот результаты:
Comparison:
tr!(':.', '_') : 388004.6 i/s
tr!(':.', '__') : 376692.5 i/s - 1.03x slower
tr!(':', '_').tr!('.', '_') : 292588.6 i/s - 1.33x slower
tr(':.', '_') : 287673.0 i/s - 1.35x slower
tr(':.', '__') : 281670.7 i/s - 1.38x slower
tr(':', '_').tr('.', '_') : 194984.3 i/s - 1.99x slower
gsub('.', '_').gsub(':', '_') : 140625.4 i/s - 2.76x slower
gsub!('.', '_').gsub!(':', '_'): 135940.1 i/s - 2.85x slower
gsub(/[:.]/, '_') : 107319.9 i/s - 3.62x slower
gsub(/:|\./, '_') : 106426.4 i/s - 3.65x slower
gsub!(/[:.]/, '_') : 104080.7 i/s - 3.73x slower
*gsub!(/:|\./, '_') : 103308.7 i/s - 3.76x slower
Звёздочкой отмечена оригинальная версия. Как и следовало ожидать, tr
с друзьями разгромил gsub
-ы. Также легко заметить что gsub
со строкой в качестве первого аргумента рвёт регексповый вариант.
Легко заметить, что двоиточия и точки могут быть только в t
, поэтому достаточно tr
прогонять только по этой подстроке. Попробуем:
Benchmark.ips do |x|
t = '2005-08-09T18:31:42.201'
guid = '644e1dd7-2a7f-18fb-b8ed-ed78c3f92c2b'
x.report("global tr!") { %(App-#{guid}-#{t.dup}).tr!(':.', '_') }
x.report("local tr!") { %(App-#{guid}-#{t.dup.tr!(':.', '_')}) }
x.compare!
end
__END__
Comparison:
local tr!: 325396.0 i/s
global tr!: 306451.9 i/s - 1.06x slower
Можно попробовать ещё по хардкору ускорить. Мы-то знаем, где в t
двоеточия и точки, правильно? Вот туда и вкостылим подчёркивания.
Benchmark.ips do |x|
guid = '644e1dd7-2a7f-18fb-b8ed-ed78c3f92c2b'
x.report("[]=") {
t = '2005-08-09T18:31:42.201'
t[13] = t[16] = t[19] = '_'
%(App-#{guid}-#{t})
}
x.report("tr!") {
t = '2005-08-09T18:31:42.201'
%(App-#{guid}-#{t.tr!(':.', '_')})
}
x.compare!
end
__END__
Comparison:
tr!: 376810.5 i/s
[]=: 358605.9 i/s - 1.05x slower
Провал. Можно ещё попробовать зафризить строковые константы.
Benchmark.ips do |x|
guid = '644e1dd7-2a7f-18fb-b8ed-ed78c3f92c2b'
x.report("freeze inline") {
t = '2005-08-09T18:31:42.201'
%(App-#{guid}-#{t.tr!(':.'.freeze, '_'.freeze)})
}
CD = ':.'.freeze
U = '_'.freeze
x.report("global freeze") {
t = '2005-08-09T18:31:42.201'
%(App-#{guid}-#{t.tr!(CD, U)})
}
x.report('no freeze') {
t = '2005-08-09T18:31:42.201'
%(App-#{guid}-#{t.tr!(':.', '_')})
}
x.compare!
end
__END__
Comparison:
global freeze: 428366.0 i/s
freeze inline: 426449.9 i/s - 1.00x slower
no freeze: 373307.3 i/s - 1.15x slower
Победно. Тут, кстати, заметно, что срабатывает костыль, добавленный в руби 2, связанный с заморозкой строковых литералов. Когда руби парсер видит замороженный литерал, он делает свою чёрную магию, и под мороженый литерал память выделяется только один раз, даже если его морозить в цикле. В 1.9.3 такого не было! В качестве пруфа - результаты этого же бенчмарка, но для руби 1.9.3:
Comparison:
global freeze: 360031.1 i/s
no freeze: 318148.6 i/s - 1.13x slower
freeze inline: 282103.0 i/s - 1.28x slower
И, наконец, сравним победителя и оригинальную версию:
Benchmark.ips do |x|
guid = '644e1dd7-2a7f-18fb-b8ed-ed78c3f92c2b'
x.report("before") {
t = '2005-08-09T18:31:42.201'
%(App-#{guid}-#{t}).gsub!(/:|\./, "_")
}
x.report("after") {
t = '2005-08-09T18:31:42.201'
%(App-#{guid}-#{t.tr!(':.'.freeze, '_'.freeze)})
}
x.compare!
end
__END__
Comparison:
after: 428007.6 i/s
before: 99713.1 i/s - 4.29x slower
Лютый вин. Вот полный код победителя:
def base_zip_log_name
t = Time.now.utc.iso8601
# Name the file based on GUID and time. GUID and Date/time of the request are as close to unique filename as we're going to get
%(App-#{guid}-#{t.tr!(':.'.freeze, '_'.freeze)})
end
Правда, ужасно?