Оптимизация кода ruby для разбора строки на числовое значение
В настоящее время я работаю над некоторыми тестами скорости для ruby, и мне нужно проанализировать некоторые текстовые файлы по числовым значениям. Из-за медленной скорости мне было интересно, можно ли оптимизировать мой код, или действительно ли ruby такой медленный. Код читается из файлов, эти файлы содержат около 1 000 000 случайно сгенерированных строк или цифр, я покажу только несколько строк, чтобы вы знали, что читается. Имена файлов, которые мне нужно прочитать, передаются в качестве аргументов, coed - это отдельные сценарии (просто для моей ясности).
Сначала я хочу разобрать простое число, входные данные поступают в следующем формате:
type
number
type
number
...
Вот как я это сделал:
incr = 1
File.open(ARGV[0], "r").each_line do |line|
incr += 1
if incr % 3 == 0
line.to_i
end
end
Во-вторых, мне нужно проанализировать один список, входные данные поступают в следующем формате:
type
(1,2,3,...)
type
(1,2,3,...)
...
Вот как я это сделал
incr = 1
File.open(ARGV[0], "r").each_line do |line|
incr += 1
if incr % 3 == 0
line.gsub("(","").gsub(")","").split(",").map{ |s| s.to_i}
end
end
Наконец, мне нужно проанализировать список списков, входные данные поступают в следующем формате:
type
((1,2,3,...),(1,2,3,...),(...))
type
((1,2,3,...),(1,2,3,...),(...))
...
Вот как я это сделал:
incr = 1
File.open(ARGV[0], "r").each_line do |line|
incr += 1
if incr % 3 == 0
line.split("),(").map{ |s| s.gsub("(","").gsub(")","").split(",").map{ |s| s.to_i}}
end
end
Мне не нужно отображать какие-либо результаты, я просто тестирую скорость, поэтому нет необходимости в выводе. Я проверил результат, и сами коды, кажется, работают правильно, они просто удивительно медленны, и я хотел бы ускорить тестирование с оптимальным предложением рубина. Я знаю, что есть несколько скоростных тестов, которые я мог бы использовать, но для своих целей мне нужно создать свои собственные.
Что я могу сделать лучше? Как можно оптимизировать этот код? Где я ошибся, или это уже лучший рубин? Заранее благодарю за советы и идеи.
2 ответа
В первом вместо:
File.open(ARGV[0], "r").each_line do |line|
Использование:
File.foreach(ARGV[0]) do |line|
И вместо:
incr += 1
if incr % 3 == 0
Использование:
if $. % 3 == 0
$.
является магической переменной для номера строки последней прочитанной строки.
Во втором вместо:
line.gsub("(","").gsub(")","").split(",").map{ |s| s.to_i}
Использование:
line.tr('()', '').split(',').map(&:to_i)
В третьем вместо:
line.split("),(").map{ |s| s.gsub("(","").gsub(")","").split(",").map{ |s| s.to_i}}
Использование:
line.scan(/(?:\d+,?)+/).map{ |s| s.split(',', 0).map(&:to_i) }
Вот как работает эта строка:
line.scan(/(?:\d+,?)+/)
=> ["1,2,3,", "1,2,3,"]
line.scan(/(?:\d+,?)+/).map{ |s| s.split(',',0) }
=> [["1", "2", "3"], ["1", "2", "3"]]
line.scan(/(?:\d+,?)+/).map{ |s| s.split(',', 0).map(&:to_i) }
=> [[1, 2, 3], [1, 2, 3]]
Я не проводил никаких тестов для сравнения скорости, но изменения должны быть быстрее, потому что gsub
звонки ушли. Внесенные мною изменения не обязательно были самыми быстрыми способами, это были более оптимизированные версии вашего собственного кода.
Попытка сравнить скорость Ruby с другими языками требует знания самых быстрых способов выполнения каждого шага, основываясь на нескольких тестах этого шага. Это также означает, что вы работаете на идентичном оборудовании и ОС, и все ваши языки скомпилированы в наиболее эффективные для скорости формы. Языки делают компромисс между использованием памяти и скоростью, поэтому, хотя один может быть медленнее другого, он также может быть более эффективным с точки зрения памяти.
Кроме того, при кодировании в производственной среде время для создания кода, который работает правильно, должно быть учтено в уравнении "что быстрее". C очень быстр, но для написания программ занимает больше времени, чем Ruby, для большинства задач, потому что C не держит вас за руку, как Ruby. Что быстрее, когда для написания и отладки кода C требуется неделя, а для кода Ruby - час? Просто о чем подумать.
Я не прочитал ответ @ tadman и комментарии, пока не закончил. С помощью:
map(&:to_i)
раньше был медленнее чем:
map{ |s| s.to_i }
Разница в скорости зависит от используемой вами версии Ruby. Первоначально используя &:
был реализован в некоторых обезьян-патчах, но теперь он встроен в Ruby. Когда они внесли это изменение, оно значительно ускорилось:
require 'benchmark'
foo = [*('1'..'1000')] * 1000
puts foo.size
N = 10
puts "N=#{N}"
puts RUBY_VERSION
puts
Benchmark.bm(6) do |x|
x.report('&:to_i') { N.times { foo.map(&:to_i) }}
x.report('to_i') { N.times { foo.map{ |s| s.to_i } }}
end
Какие выводы:
1000000
N=10
2.0.0
user system total real
&:to_i 1.240000 0.000000 1.240000 ( 1.250948)
to_i 1.400000 0.000000 1.400000 ( 1.410763)
Это проходит 10 000 000 элементов, что приводит только к разнице в.2/ сек. Это не большая разница между двумя способами сделать одно и то же. Если вы собираетесь обрабатывать гораздо больше данных, это имеет значение. Для большинства приложений это спорный вопрос, потому что другие вещи будут узкими местами / замедлениями, поэтому пишите код любым удобным для вас способом, с учетом этой разницы в скорости.
Чтобы показать разницу, которую делает версия Ruby, вот те же результаты тестов с использованием Ruby 1.8.7:
1000000 N = 10 1.8.7 пользовательская система всего реального &:to_i 4,940000 0,000000 4,940000 ( 4,945604) to_i 2,390000 0,000000 2,390000 ( 2,396693)
Так далеко как gsub
против tr
:
require 'benchmark'
foo = '()' * 500000
puts foo.size
N = 10
puts "N=#{N}"
puts RUBY_VERSION
puts
Benchmark.bm(6) do |x|
x.report('tr') { N.times { foo.tr('()', '') }}
x.report('gsub') { N.times { foo.gsub(/[()]/, '') }}
end
С этими результатами:
1000000 N = 10 1.8.7 пользовательская система всего реального tr 0,010000 0,000000 0,010000 ( 0,011652) gsub 3.010000 0.000000 3.010000 ( 3.014059)
а также:
1000000 N = 10 2.0.0 пользовательская система всего реального tr 0,020000 0,000000 0,020000 ( 0,017230) gsub 1.900000 0.000000 1.900000 ( 1.904083)
Вот то отличие, которое мы можем видеть из изменения шаблона регулярных выражений, которое вызывает изменения в обработке, необходимой для получения желаемого результата:
require 'benchmark'
line = '((1,2,3),(1,2,3))'
pattern1 = /\([\d,]+\)/
pattern2 = /\(([\d,]+)\)/
pattern3 = /\((?:\d+,?)+\)/
pattern4 = /\d(?:[\d,])+/
line.scan(pattern1) # => ["(1,2,3)", "(1,2,3)"]
line.scan(pattern2) # => [["1,2,3"], ["1,2,3"]]
line.scan(pattern3) # => ["(1,2,3)", "(1,2,3)"]
line.scan(pattern4) # => ["1,2,3", "1,2,3"]
line.scan(pattern1).map{ |s| s[1..-1].split(',').map(&:to_i) } # => [[1, 2, 3], [1, 2, 3]]
line.scan(pattern2).map{ |s| s[0].split(',').map(&:to_i) } # => [[1, 2, 3], [1, 2, 3]]
line.scan(pattern3).map{ |s| s[1..-1].split(',').map(&:to_i) } # => [[1, 2, 3], [1, 2, 3]]
line.scan(pattern4).map{ |s| s.split(',').map(&:to_i) } # => [[1, 2, 3], [1, 2, 3]]
N = 1000000
Benchmark.bm(8) do |x|
x.report('pattern1') { N.times { line.scan(pattern1).map{ |s| s[1..-1].split(',').map(&:to_i) } }}
x.report('pattern2') { N.times { line.scan(pattern2).map{ |s| s[0].split(',').map(&:to_i) } }}
x.report('pattern3') { N.times { line.scan(pattern3).map{ |s| s[1..-1].split(',').map(&:to_i) } }}
x.report('pattern4') { N.times { line.scan(pattern4).map{ |s| s.split(',').map(&:to_i) } }}
end
На Ruby 2.0-p427:
user system total real
pattern1 5.610000 0.010000 5.620000 ( 5.606556)
pattern2 5.460000 0.000000 5.460000 ( 5.467228)
pattern3 5.730000 0.000000 5.730000 ( 5.731310)
pattern4 5.080000 0.010000 5.090000 ( 5.085965)
Не совсем ясно, где ваши проблемы с производительностью, но с точки зрения реализации, есть несколько вещей, которые явно не оптимальны.
Если вы ищете и заменяете, чтобы удалить определенные символы, избегайте gsub
несколько раз. Это займет значительное время, чтобы обработать и повторно обработать одну и ту же строку для каждого символа. Вместо этого сделайте это за один проход:
s.gsub(/[\(\)]/, '')
[...]
нотация внутри регулярного выражения означает "набор следующих символов", поэтому в этом случае это либо открытая, либо закрывающая скобка.
Еще более эффективным методом является tr
метод, который предназначен для переназначения или удаления отдельных символов и, как правило, намного быстрее, поскольку регулярное выражение не компилируется и не выполняется:
s.tr('()', '')
Другой трюк, если вы видите шаблон, где у вас есть блок, состоящий из вызова метода без аргументов:
map { |x| x.to_i }
Это сворачивается в краткую форму:
map(&:to_i)
Я не уверен, что эти тесты быстрее, но я не удивлюсь, если это произойдет. Это внутренний процесс.
Если вас беспокоит абсолютная скорость, вы всегда можете использовать чувствительную к производительности часть как расширение C или C++ для Ruby. Другим вариантом является использование JRuby с некоторым Java для выполнения тяжелой работы, если это лучше подходит, хотя обычно C выходит на первое место для работы низкого уровня, подобной этой.