Pull to refresh

Comments 38

Спасибо за ссылку. Моя статья не претендует на такой объем исследования и сравнения. Только лишь небольшая частичка.
Ну, может, тогда не надо такие громкие заголовки писать? Содержание статьи не соответствует названию. Отсюда и минусы… (на момент написания комментария голосовалка статьи показывает -2)
Если уж вы оптимизируете такие вещи, то, имхо, лучше взять md5, вместо sha1, он вроде как быстрее считается. А еще лучше что-нить менее криптографическое, но с равномерным распределением, типа crc32, и не будет проблем с размером int.
Согласен с вами, но исторически сложилось, что используется именно sha1. Поэтому и на Go приходится с ним работать.
В случае с Ruby используется OpenSSL (если библиотека установлена), а это значит, что при подсчете SHA1 и MD5 используются нативные инструкции процессора (если он поддерживает AES instruction set и версия OpenSSL > 1.0.1) и особой разницы нету. В данном случае, большая часть времени расходуется на аллокацию объектов в MRI.

С другой стороны, реализация SHA1 в Go написана на самом Go (даже без Go asm). Так что трудно сказать без профилирования насколько Go реально быстрее в этом случае.
про реализацию на Go — github.com/golang/go/tree/master/src/crypto/sha1, файлики *.s это ассемблеры
в Go в этом тесте тоже процентов 30 времени как раз на аллокацию и конкатенацию строк тратится
О файлики эти пропустил, да. В любом случае, в ассемблерных файлах нету упоминания Intel SHA Extensions (выше я почему-то посчитал, что поддержка SHA быть в AES Instruction Set, но она реализована как часть SSE), а значит все равно некоректно сравнивать эти реализации только по общему времени исполнения. Надо смотреть на реальное время выполнения каждой функции/метода.
Спасибо за интересную ссылку. В свободное время надо будет попробовать.
> Но на Ruby такой трюк выполнить не удалось.
Возможно все
text = 'asd'
Digest::SHA1.digest(text)[-1].ord & 0x7 == Integer('0x'+Digest::SHA1.hexdigest(text))%8 # true
UFO just landed and posted this here
Точно. Возможно, что в вашем случае какой-то быстрый процессор используется. Я у себя тестировал на обычном ноутбуке.

И обратите внимание сколько памяти потребляется в вашем случае. В среднем 20 MiB и для hhvm почти 80! У меня Go 1,5, а Ruby 2,6 МБ использовали.
чтобы говорить, что код быстрее нужно сравнивать одинаковые вещи,
об первой вещи собственно по вашей ссылке и написано go работает с unicode
а вторая, что время для go включает ещё и компиляцию: github.com/dimroc/etl-language-comparison/blob/master/run_go
про Go
~30% времени тратится на аллокации строк в цикле
также используйте sha1.Sum([]byte(text)) это будет быстрее:

mas := sha1.Sum([]byte(text))
return int64(mas[len(mas)-1]) % 8

также если вы не используете в коде горутины то GOMAXPROCS на вашем коде никак не скажется
ну и тесты производительности в Go принято делать через bench (http://dave.cheney.net/2013/06/30/how-to-write-benchmarks-in-go)
оригинальный код:
node1: 1118402612
node2: 772230494

использование sha1.Sum (без пре аллокации)
node1: 837969082
node2: 485179839

пре-аллоцированные данные
node1: 738665390
node2: 478114103

пре-аллокация и sha1.Sum:
node1: 545527947
node2: 286142563
на тойже машине, PHP 5.6.7-1 3v4l.org/moMXQ
0.73778009414673
Интерпретируемый против компилируемого. Действительно, что же быстрее…
Стоит ли вобще начинать сравнивать две платформы если оригинальный код настолько глуп (я только про код). Хэшировать строку в big integer, конвертировать в hex string, конкатенировать с «0x», парсить обратно в число О_о Чудовищный подход.

crc32, у которого более подходящий интерфейс для этого (возвращает число), работает в 5.5 раза — о, чудо — быстрее:

Скрытый текст
require 'digest'
require 'zlib'

max = 2000000

def measure(title)
  start = Time.now
  yield if block_given?
  elapse = Time.now - start
  puts "#{title}: #{elapse} sec"
end

measure("with_crc32") {
	(0..max).each do |i|
		Zlib.crc32("text #{i}") % 8
	end
}

measure("with_sha1") {
	(0..max).each do |i|
		Integer('0x'+Digest::SHA1.hexdigest("text #{i}"))%8
	end
}

Можете не переходить на Go, я ваш пример на руби почти в 2 раза ускорил:

require 'benchmark/ips'
require 'digest'

text = 'Preved, medved!!!'

Benchmark.ips do |x|
x.report('habr:') { Integer('0x'+Digest::SHA1.hexdigest(text))%8 }
x.report('my_optimization:') { Digest::SHA1.digest(text)[-1].ord & 7 }
end

===

ruby 1.rb
Calculating — habr: 26.803k i/100ms
my_optimization: 41.454k i/100ms

— habr: 359.737k (± 0.9%) i/s — 1.823M
my_optimization: 646.808k (± 1.1%) i/s — 3.233M

1. Преобразовывать весь хэш в число нет смысла, если вам всего-то надо остаток от деления на 8 узнать
2. Самый быстрый способ узнать остаток от деления — логическое AND с 3 последними битами
Вчера пользователь printercu в комментарии выше как раз и предложил такой вариант ускорения. Он получается почти в 2 раза быстрее чем оригинальный код.
Ааа, ну я кода не увидел и бенчмаркопопугаев, потому, наверное, просмотрел его вариант (надо было хобя бы сослаться на него в самой статье.

P.S. да, про crc32 он верно предложил.
Ещё добавлю:
учитывая что на моём компе редис работает со скоростью 73855.24 requests per second
вы уверены что вам надо оптимизировать именно функцию хеширования?
У меня на серверах есть несколько скриптов на руби, которые с определенной периодичностью обращаются к редису и что-то делают. Например, считают рейтинги игроков, выдают бонусы, чистят старые данные из редиса и т.д.

И я решил переписать часть этих скриптов на Go (чтобы более детально познакомится с языком с прицелом для будущих проектов). В процессе переписывания мне и стало интересно — на сколько Go быстрее Ruby в этой задаче.

Поэтому у меня особой задачи по оптимизации функции хеширования и не было (и так все достаточно быстро работает). Только спортивный интерес, который и перерос в этот топик.
Кстати, запустил ваш код на Go у себя на компе, получилось ~1 млн/сек хешей на node2 функции. Для языка с строгой типизацией думал круче будет (видимо там ещё что-то можно улучшить).
Благодаря комментариям, это вариант самый быстрый на go:
func node3 (text string) int64 {
	mas := sha1.Sum([]byte(text))
	return int64(mas[len(mas)-1]) % 8
}
Пришлось посмотреть как бенчмаркать в Go… не так круто как у Ruby…
~2_347_417 / s на моём компе
Уже нормально.
Эм. Даже без всякой магии с битовыми масками, вы 25% скорости тратите тупо на конкатенацию строк.

require 'digest'
require 'benchmark'

text = 'Hello world'
N=1_000_000
Benchmark.bm do |x|
  x.report{ N.times{ Integer('0x'+Digest::SHA1.hexdigest(text)) } }
  x.report{ N.times{ Digest::SHA1.hexdigest(text).to_i(16) } }
end


user system total real
3.360000 0.000000 3.360000 ( 3.373353)
2.600000 0.000000 2.600000 ( 2.593256)

Если вам было так очевидно, что не надо конкатенировать строку, чтобы перевести ее в HEX, а пользоваться стандартными методами, то зачем вы это делали?
Ну а серьезно что вы этим бенчмарком показали? Кроме того, что вы плохой ruby разработчик?

Все нормальные разработчики на руби знают, что создавать строку на руби длинее 23 символов лишний раз не надо.
Нормальный разработчик на руби, Вы где больше 23 символов насчитали?
На Perl получилось:

sub aaa ($text) {
    return hex( substr Digest::SHA1::sha1_hex($text), -2, 1 ) % 8;
}

Benchmark: timing 1000000 iterations of node1...
     node1:  1 wallclock secs ( 0.80 usr +  0.00 sys =  0.80 CPU) @ 1254705.14/s (n=1000000)
           Rate node1
node1 1254705/s    --
Sign up to leave a comment.

Articles