Pull to refresh

Comments 26

вот бы подробностей что там под капотом
Только вчера искал что-нибудь про sync.Map (очень уж смущала фраза в официальной документации про «стабильность»), и тут по подписке готовый и разжеванный ответ! Спасибо за разъяснение.

При прочтении статьи закралась мысль, а не может ли оказаться так, что вместо map+RWMutex лучше себя проявит map+Mutex в ситуации cache contention или близкой к ней?

You're welcome!


При прочтении статьи закралась мысль, а не может ли оказаться так, что вместо map+RWMutex лучше себя проявит map+Mutex в ситуации cache contention или близкой к ней?

Ну, для этого (*sync.Mutex).Lock() + чтение из map + (*sync.Mutex).Unlock() должны быть быстрее, чем трансфер значения из L2 кеша между ядрами CPU на данной архитектуре. Несложно замерить, впрочем.

Mutex.Lock() гарантировано содержит атомарную операцию, т.е. cache transfer там точно будет.

Простите, а можно ли ссылку на бенчмарк, с которого взят график скорости для python? Не думал что он настолько медленный, интересно посмотреть, почему так.


А, кажется это можно найти тут. Итого, мы сравниваем (если я правильно понял): "Java openjdk 1.8.0 java.util.HashMap, c++11 gcc 4.8.4 unordered_map, -O2. Python 2.7.6".


Что ж такую новую версию питона взяли, а не какой-то python 2.3? Уверен, результаты были бы еще более шокирующими.

Как показывает практика, производительность Python 3 немного ниже производительность Python 2. Такие дела.

Если верить вот этим бенчмаркам, основанным на python/performance, то зависит от того, что использовать.


Тот же pickle_dict проходит быстрее, следовательно, вполне возможно, что сам dict в python3.6 в целом работает быстрее, чем в python 2.7.6.


И даже если забить на это, почему нельзя было использовать последнюю версию python 2.7?

И даже если забить на это, почему нельзя было использовать последнюю версию python 2.7?
Потому что бенчмарки эти делались, в первую очередь, для того, чтобы принять решения. И была использована последняя доступная версия python2. То что она оказалась несколько… старше, чем фанатам бы хотелось — ну что ж тут поделать. Зато это то, что реально используется в проде.

По этой же принчине использовался gcc 4.8.4, а не gcc 7, OpenJDK 8 и так далее.

Переписывать миллионы строк на python 3 ради копеечного ускорения никто не будет. Вот если бы python3 был сравним с go или java — тогда другой вопрос, а так — разница вместо стократной станет, если повезёт, десятикратной. И то вряд ли. Если уж переписывать, то стоит взять другой язык и получить-таки стократное ускорение…

Ну так не переписывайте. Вот я взял и провел бенчмарк из этого комментария для 2.7.13. Получил аналогичные результаты стандартным %timeit, то есть 35-40 ns.
Страшные циферки из графика у меня получилось получить только низким количеством прогонов, а следовательно, реальное отставание от лидеров у python всего в 2-3 раза, что не так плохо.


Ну и да, это какое-то странное лукавство, сравнивать "реальные версии, которые есть в дистрибутиве" и новый релиз go.


Если же на python 2.7.6 в самом деле все так плохо, то уж обновить пакет ради прироста производительности то можно, я думаю.

Тоже заинтересовали цифры. Проверил:


$ ipython
Python 3.5.3 (default, Apr 22 2017, 00:00:00) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: m = dict(enumerate('abcdefghij'))

In [2]: %timeit m[4]
38.1 ns ± 0.247 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

In [4]: m = dict((v, v) for v in 'abcdefghij')

In [5]: %timeit m['c']
35.6 ns ± 0.346 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)

Интересно, в каки условиях могло получиться 150 ns.

А потому что нужно делать вот так:


Python 3.6.2 (default, Jul 20 2017, 03:52:27) 
Type 'copyright', 'credits' or 'license' for more information
IPython 6.1.0 -- An enhanced Interactive Python. Type '?' for help.

In [1]: m = dict(enumerate('abcdefghij'))
   ...: 

In [2]: %timeit -n 15 m[4]
265 ns ± 43.7 ns per loop (mean ± std. dev. of 7 runs, 15 loops each)

Можно попрогонять так раз 20 и получить совершенно фантатические результаты, от 36.3 до 265.

Я думаю все немного проще. Можно потестировать так
In [1]: m = dict(enumerate('abcdefghij'))
In [2]:
%%timeit x = 0
x = x + 7
if x > 9: x = 0
m[x]

10000000 loops, best of 3: 58.7 ns per loop

А можно и так
In [3]: import random
In [4]: 
%%timeit      
m[random.randint(0, 9)]

1000000 loops, best of 3: 708 ns per loop

Сами по себе хеш-таблицы в Python очень быстрые, что вполне логично для языка, где любой объект по сути хеш таблица. Думюа в Python они побыстрее, нежели в Go.
Но вот обвязочка медленная. Скажем банальный вызов random.randint добавляет еще одно чтение из хеш-таблицы random и т.п. Поэтому все подобные «бенчмарки» без исходников не стоят и выеденного яйца.
Можно попрогонять так раз 20 и получить совершенно фантатические результаты, от 36.3 до 265.

Я получаю либо 0, либо 63ns. Если посмотреть time.get_clock_info('clock'), будет видно, что разрешение таймера 1e-06 секунд, то есть 1000 наносекунд. Проверяем:


In [35]: (time.clock() - time.clock()) * 1e9
Out[35]: -1000.000000139778

1000ns / 15 раз = 66.6ns. Так что замерять время выполнения таких быстрых операций отдельно или даже по 15 штук нет никакого смысла — время выполнения меньше разрешения таймера.

В текущей реализации map очень быстр
Но медленнее Java примерно на 15-40% (на глаз по графику)? Было инетересно взглянуть на бенчмарки.
Ну для nodejs там результаты ожидаемые — идет внутренняя оптимизация для объектов с целочисленными индексами. В комментариях это упоминается github.com/golang/go/issues/19495#issuecomment-289293859
Хотя выигрыш PyPy в этом бенчмарке выглядит забавно :)

Ситуация для Java иная — хеш таблицы не имеют поддержки на уровне языка и реализованы как часть стандартной библиотеки. По идее реализация на уровне языка (go) должна быть побыстрее… Поэтому и интересно было бы взглянуть на бенчмарки.
Есть глупый вопрос автору. В статье «ядро» это поток или физическое ядро. Intel i7 4 ядра с гипертрейдингом это 8 потоков или 8 ядер?
Это «поток» — тоесть то значение количества CPU, которое видно операционной системе. runtime.NumCPU(), другими словами.

Интересно, а есть ли возможность прикрепить пул горутин к одному потоку ОС?
В этом случае синхронизация внутри такого пула не нужна по построению.

Это слишком сурово, особенно с учетом


Until the calling goroutine exits or calls UnlockOSThread, it will always execute in that thread, and no other goroutine can.

Хочется провернуть фокус, который легко доступен, скажем, с файберами на Windows — IO параллелизм без синхронизации.

А затраты на приведение типа (из interface{} в конкретный) учитывались при подсчете разницы во времени между map[...]… и sync.Map?

Да, если вы про график вначале статьи, то там учтены (он из статьи по ссылкам в разделе Ссылки).


Оверхед от конвертации в и из интерфейсом можно померять бенчмарками из стандартной библиотеки:


go test -test.run none -bench Conv runtime

У меня вот такой результат выдает (T — type, I — interface, E — empty interface):


$ go test -test.run none -bench Conv runtime
goos: darwin
goarch: amd64
pkg: runtime
BenchmarkConvT2ESmall-4         500000000            3.50 ns/op
BenchmarkConvT2EUintptr-4       500000000            3.49 ns/op
BenchmarkConvT2ELarge-4         50000000            26.3 ns/op
BenchmarkConvT2ISmall-4         500000000            3.44 ns/op
BenchmarkConvT2IUintptr-4       500000000            3.43 ns/op
BenchmarkConvT2ILarge-4         50000000            25.5 ns/op
BenchmarkConvI2E-4              2000000000           1.12 ns/op
BenchmarkConvI2I-4              200000000            9.91 ns/op
BenchmarkConvT2Ezero/zero/16-4          500000000            3.53 ns/op
BenchmarkConvT2Ezero/zero/32-4          500000000            3.53 ns/op
BenchmarkConvT2Ezero/zero/64-4          500000000            3.42 ns/op
BenchmarkConvT2Ezero/zero/str-4         500000000            3.35 ns/op
BenchmarkConvT2Ezero/zero/slice-4       500000000            3.33 ns/op
BenchmarkConvT2Ezero/zero/big-4         20000000            99.7 ns/op
BenchmarkConvT2Ezero/nonzero/16-4       100000000           14.2 ns/op
BenchmarkConvT2Ezero/nonzero/32-4       100000000           15.0 ns/op
BenchmarkConvT2Ezero/nonzero/64-4       100000000           17.6 ns/op
BenchmarkConvT2Ezero/nonzero/str-4      50000000            30.4 ns/op
BenchmarkConvT2Ezero/nonzero/slice-4    50000000            36.3 ns/op
BenchmarkConvT2Ezero/nonzero/big-4      20000000            93.4 ns/op
PASS
ok      runtime 38.947s
Sign up to leave a comment.

Articles