Что такое _md5.md5 и почему hashlib.md5 намного медленнее?

Нашел этот недокументированный _md5, когда разочаровался в медленной реализации stdlib hashlib.md5.

На макбуке:

>>> timeit hashlib.md5(b"hello world")
597 ns ± 17.2 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>> timeit _md5.md5(b"hello world")
224 ns ± 3.18 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>> _md5
<module '_md5' from '/usr/local/Cellar/python/3.7.6_1/Frameworks/Python.framework/Versions/3.7/lib/python3.7/lib-dynload/_md5.cpython-37m-darwin.so'>

В окне Windows:

>>> timeit hashlib.md5(b"stonk overflow")
328 ns ± 21.8 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>> timeit _md5.md5(b"stonk overflow")
110 ns ± 12.5 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
>>> _md5
<module '_md5' (built-in)>

На Linux-боксе:

>>> timeit hashlib.md5(b"https://adventofcode.com/2016/day/5")
259 ns ± 1.33 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
>>> timeit _md5.md5(b"https://adventofcode.com/2016/day/5")
102 ns ± 0.0576 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
>>> _md5
<module '_md5' from '/usr/local/lib/python3.8/lib-dynload/_md5.cpython-38-x86_64-linux-gnu.so'>

Для хеширования коротких сообщений это намного быстрее. Для длинных сообщений аналогичная производительность.

Почему он скрыт в модуле расширения подчеркивания и почему эта более быстрая реализация не используется по умолчанию в hashlib? Что такое модуль _md5 и почему у него нет общедоступного API?


person wim    schedule 28.01.2020    source источник
comment
Какую относительную производительность вы получите на более длинных строках (например, 5 мегабайт)?   -  person Jeremy Friesner    schedule 28.01.2020
comment
Интересно - с 5 МБ urandom производительность кажется похожей (в пределах погрешности). Но для майнинга AdventCoin мне нужно было хешировать короткие сообщения..   -  person wim    schedule 28.01.2020
comment
Я предполагаю, что в результатах тестов с небольшими строками преобладают накладные расходы на настройку вызова, а не сам алгоритм.   -  person Jeremy Friesner    schedule 28.01.2020


Ответы (3)


До Python 2.5 хэши и дайджесты были реализованы в отдельных модулях (например, [Python 2.Docs]: md5 — алгоритм дайджеста сообщений MD5).
Начиная с v2.5, [Python 2.6. Docs]: hashlib — добавлены безопасные хэши и дайджесты сообщений. Его целью было:

  1. Предложите унифицированный метод доступа к хэшам/дайджестам (через их имя)
  2. Переключитесь (по умолчанию) на внешнего поставщика криптографии (кажется логичным шагом делегировать полномочия какой-либо организации, специализирующейся в этой области, поскольку поддержка всех этих алгоритмов может оказаться излишним). В то время OpenSSL был лучшим выбором: достаточно зрелый, известный и совместимый (была куча похожих провайдеров Java, но они были довольно бесполезны).

В качестве побочного эффекта #2. реализации Python были скрыты от общедоступного API (их переименовали: _md5, _sha1, _sha256, _sha512, а последние добавлены: _blake2 , _sha3), так как избыточность часто приводит к путанице.
Но другим побочным эффектом была зависимость _hashlib.so от OpenSSL libcrypto*.so ( это специфично для Nix (как минимум Lnx), в Win статическая libeay32.lib была связана в < em>_hashlib.pyd, а также _ssl.pyd (который я считаю хромым), до v3.7+, где OpenSSL .dll являются частью установки Python).
Вероятно, на 90+% машин все было гладко, так как OpenSSL был/установлен по умолчанию, но на тех, где это не так, многие вещи могут ломаются, потому что, например, hashlib импортируется многими модулями (один из таких примеров — случайный, который сам импортируется множеством других), поэтому тривиальные фрагменты кода, которые вообще не связаны с криптографией (по крайней мере, на первыйst взгляд) перестанут работать. Вот почему старые реализации сохраняются (но опять же, они являются лишь запасными вариантами, поскольку версии OpenSSL поддерживаются/должны поддерживаться лучше).

[cfati@cfati-ubtu16x64-0:~/Work/Dev/StackOverflow/q059955854]> ~/sopr.sh
*** Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ***

[064bit-prompt]> python3 -c "import sys, hashlib as hl, _md5, ssl;print(\"{0:}\n{1:}\n{2:}\n{3:}\".format(sys.version, _md5, hl._hashlib, ssl.OPENSSL_VERSION))"
3.5.2 (default, Oct  8 2019, 13:06:37)
[GCC 5.4.0 20160609]
<module '_md5' (built-in)>
<module '_hashlib' from '/usr/lib/python3.5/lib-dynload/_hashlib.cpython-35m-x86_64-linux-gnu.so'>
OpenSSL 1.0.2g  1 Mar 2016
[064bit-prompt]>
[064bit-prompt]> ldd /usr/lib/python3.5/lib-dynload/_hashlib.cpython-35m-x86_64-linux-gnu.so
        linux-vdso.so.1 =>  (0x00007fffa7d0b000)
        libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f50d9e4d000)
        libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f50d9a83000)
        libcrypto.so.1.0.0 => /lib/x86_64-linux-gnu/libcrypto.so.1.0.0 (0x00007f50d963e000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f50da271000)
        libdl.so.2 => /lib/x86_64-linux-gnu/libdl.so.2 (0x00007f50d943a000)
[064bit-prompt]>
[064bit-prompt]> openssl version -a
OpenSSL 1.0.2g  1 Mar 2016
built on: reproducible build, date unspecified
platform: debian-amd64
options:  bn(64,64) rc4(16x,int) des(idx,cisc,16,int) blowfish(idx)
compiler: cc -I. -I.. -I../include  -fPIC -DOPENSSL_PIC -DOPENSSL_THREADS -D_REENTRANT -DDSO_DLFCN -DHAVE_DLFCN_H -m64 -DL_ENDIAN -g -O2 -fstack-protector-strong -Wformat -Werror=format-security -Wdate-time -D_FORTIFY_SOURCE=2 -Wl,-Bsymbolic-functions -Wl,-z,relro -Wa,--noexecstack -Wall -DMD32_REG_T=int -DOPENSSL_IA32_SSE2 -DOPENSSL_BN_ASM_MONT -DOPENSSL_BN_ASM_MONT5 -DOPENSSL_BN_ASM_GF2m -DSHA1_ASM -DSHA256_ASM -DSHA512_ASM -DMD5_ASM -DAES_ASM -DVPAES_ASM -DBSAES_ASM -DWHIRLPOOL_ASM -DGHASH_ASM -DECP_NISTZ256_ASM
OPENSSLDIR: "/usr/lib/ssl"
[064bit-prompt]>
[064bit-prompt]> python3 -c "import _md5, hashlib as hl;print(_md5.md5(b\"A\").hexdigest(), hl.md5(b\"A\").hexdigest())"
7fc56270e7a70fa81a5935b72eacbe29 7fc56270e7a70fa81a5935b72eacbe29

Согласно [Python 3.Docs]: hashlib.algorithms_guaranteed< /сильный>:

Набор, содержащий имена алгоритмов хеширования, которые гарантированно поддерживаются этим модулем на всех платформах. Обратите внимание, что «md5» есть в этом списке, несмотря на то, что некоторые поставщики исходных продуктов предлагают странную «совместимую с FIPS» сборку Python, которая исключает его.

Ниже приведен пример пользовательской установки Python 2.7 (которую я создал довольно давно, стоит отметить, что она динамически связывается с OpenSSL .dll):

e:\Work\Dev\StackOverflow\q059955854>sopr.bat
*** Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ***

[prompt]> "F:\Install\pc064\HPE\OPSWpython\2.7.10__00\python.exe" -c "import sys, ssl;print(\"{0:}\n{1:}\".format(sys.version, ssl.OPENSSL_VERSION))"
2.7.10 (default, Mar  8 2016, 15:02:46) [MSC v.1600 64 bit (AMD64)]
OpenSSL 1.0.2j-fips  26 Sep 2016

[prompt]> "F:\Install\pc064\HPE\OPSWpython\2.7.10__00\python.exe" -c "import hashlib as hl;print(hl.md5(\"A\").hexdigest())"
7fc56270e7a70fa81a5935b72eacbe29

[prompt]> "F:\Install\pc064\HPE\OPSWpython\2.7.10__00\python.exe" -c "import ssl;ssl.FIPS_mode_set(True);import hashlib as hl;print(hl.md5(\"A\").hexdigest())"
Traceback (most recent call last):
  File "<string>", line 1, in <module>
ValueError: error:060A80A3:digital envelope routines:FIPS_DIGESTINIT:disabled for fips

Что касается вопроса скорости, я могу только предполагать:

  • Реализация Python была (очевидно) написана специально для Python, что означает, что она "более оптимизирована" (да, это грамматически неправильно) для Python, чем универсальная версия, а также находится в python*.so (или в самом исполняемом файле python)
  • Реализация OpenSSL находится в libcrypto*.so, и к ней обращается оболочка _hashlib.so, которая выполняет обратное и обратное преобразование между < em>Python (PyObject*) и OpenSSL (EVP_MD_CTX*)

Принимая во внимание вышеизложенное, логично предположить, что первый вариант (немного) быстрее (по крайней мере, для небольших сообщений, где накладные расходы (вызов функции и другие базовые операции Python) занимают значительный процент общего времени. по сравнению с самим хешированием). Также необходимо учитывать и другие факторы (например, использовались ли ускорения ассемблера OpenSSL).



Обновить #0

Ниже приведены некоторые мои собственные тесты.

code00.py:

#!/usr/bin/env python

import sys
from hashlib import md5 as md5_openssl
from _md5 import md5 as md5_builtin
import timeit


def main(*argv):
    base_text = b"A"
    number = 1000000
    print("timeit attempts number: {0:d}".format(number))
    #x = []
    #y = {}
    for count in range(0, 16):
        factor = 2 ** count
        text = base_text * factor
        globals_dict = {"text": text}
        #x.append(factor)
        print("\nUsing a {0:8d} (2 ** {1:2d}) bytes message".format(len(text), count))
        for func in [
            md5_openssl,
            md5_builtin,
        ]:
            globals_dict["md5"] = func

            t = timeit.timeit(stmt="md5(text)", globals=globals_dict, number=number)
            print("    {0:12s} took: {1:11.6f} seconds".format(func.__name__, t))
            #y.setdefault(func.__name__, []).append(t)
    #print(x, y)


if __name__ == "__main__":
    print("Python {0:s} {1:d}bit on {2:s}\n".format(" ".join(item.strip() for item in sys.version.split("\n")), 64 if sys.maxsize > 0x100000000 else 32, sys.platform))
    main(*sys.argv[1:])
    print("\nDone.")

Вывод:

  • Win 10 pc064 (на ноутбуке Dell Precision 5510):

    [prompt]> "e:\Work\Dev\VEnvs\py_pc064_03.07.06_test0\Scripts\python.exe" code00.py
    Python 3.7.6 (tags/v3.7.6:43364a7ae0, Dec 19 2019, 00:42:30) [MSC v.1916 64 bit (AMD64)] 64bit on win32
    
    timeit attempts number: 1000000
    
    Using a        1 (2 **  0) bytes message
        openssl_md5  took:    0.449134 seconds
        md5          took:    0.120021 seconds
    
    Using a        2 (2 **  1) bytes message
        openssl_md5  took:    0.460399 seconds
        md5          took:    0.118555 seconds
    
    Using a        4 (2 **  2) bytes message
        openssl_md5  took:    0.451850 seconds
        md5          took:    0.121166 seconds
    
    Using a        8 (2 **  3) bytes message
        openssl_md5  took:    0.438398 seconds
        md5          took:    0.118127 seconds
    
    Using a       16 (2 **  4) bytes message
        openssl_md5  took:    0.454653 seconds
        md5          took:    0.122818 seconds
    
    Using a       32 (2 **  5) bytes message
        openssl_md5  took:    0.450776 seconds
        md5          took:    0.118594 seconds
    
    Using a       64 (2 **  6) bytes message
        openssl_md5  took:    0.555761 seconds
        md5          took:    0.278812 seconds
    
    Using a      128 (2 **  7) bytes message
        openssl_md5  took:    0.681296 seconds
        md5          took:    0.455921 seconds
    
    Using a      256 (2 **  8) bytes message
        openssl_md5  took:    0.895952 seconds
        md5          took:    0.807457 seconds
    
    Using a      512 (2 **  9) bytes message
        openssl_md5  took:    1.401584 seconds
        md5          took:    1.499279 seconds
    
    Using a     1024 (2 ** 10) bytes message
        openssl_md5  took:    2.360966 seconds
        md5          took:    2.878650 seconds
    
    Using a     2048 (2 ** 11) bytes message
        openssl_md5  took:    4.383245 seconds
        md5          took:    5.655477 seconds
    
    Using a     4096 (2 ** 12) bytes message
        openssl_md5  took:    8.264774 seconds
        md5          took:   10.920909 seconds
    
    Using a     8192 (2 ** 13) bytes message
        openssl_md5  took:   15.521947 seconds
        md5          took:   21.895179 seconds
    
    Using a    16384 (2 ** 14) bytes message
        openssl_md5  took:   29.947287 seconds
        md5          took:   43.198639 seconds
    
    Using a    32768 (2 ** 15) bytes message
        openssl_md5  took:   59.123447 seconds
        md5          took:   86.453821 seconds
    
    Done.
    
  • Ubtu 16 pc064 (VM работает в VirtualBox на указанном выше компьютере):

    [064bit-prompt]> python3 code00.py
    Python 3.5.2 (default, Oct  8 2019, 13:06:37) [GCC 5.4.0 20160609] 64bit on linux
    
    timeit attempts number: 1000000
    
    Using a        1 (2 **  0) bytes message
        openssl_md5  took:    0.246166 seconds
        md5          took:    0.130589 seconds
    
    Using a        2 (2 **  1) bytes message
        openssl_md5  took:    0.251019 seconds
        md5          took:    0.127750 seconds
    
    Using a        4 (2 **  2) bytes message
        openssl_md5  took:    0.257018 seconds
        md5          took:    0.123116 seconds
    
    Using a        8 (2 **  3) bytes message
        openssl_md5  took:    0.245399 seconds
        md5          took:    0.128267 seconds
    
    Using a       16 (2 **  4) bytes message
        openssl_md5  took:    0.251832 seconds
        md5          took:    0.136373 seconds
    
    Using a       32 (2 **  5) bytes message
        openssl_md5  took:    0.248410 seconds
        md5          took:    0.140708 seconds
    
    Using a       64 (2 **  6) bytes message
        openssl_md5  took:    0.361016 seconds
        md5          took:    0.267021 seconds
    
    Using a      128 (2 **  7) bytes message
        openssl_md5  took:    0.478735 seconds
        md5          took:    0.413986 seconds
    
    Using a      256 (2 **  8) bytes message
        openssl_md5  took:    0.707602 seconds
        md5          took:    0.695042 seconds
    
    Using a      512 (2 **  9) bytes message
        openssl_md5  took:    1.216832 seconds
        md5          took:    1.268570 seconds
    
    Using a     1024 (2 ** 10) bytes message
        openssl_md5  took:    2.122014 seconds
        md5          took:    2.429623 seconds
    
    Using a     2048 (2 ** 11) bytes message
        openssl_md5  took:    4.158188 seconds
        md5          took:    4.847686 seconds
    
    Using a     4096 (2 ** 12) bytes message
        openssl_md5  took:    7.839173 seconds
        md5          took:    9.242224 seconds
    
    Using a     8192 (2 ** 13) bytes message
        openssl_md5  took:   15.282232 seconds
        md5          took:   18.368874 seconds
    
    Using a    16384 (2 ** 14) bytes message
        openssl_md5  took:   30.681912 seconds
        md5          took:   36.755073 seconds
    
    Using a    32768 (2 ** 15) bytes message
        openssl_md5  took:   60.230543 seconds
        md5          took:   73.237356 seconds
    
    Done.
    

Результат, кажется, сильно отличается от вашего. В моем случае:

  • Начиная где-то с сообщений размером [~512B .. ~1KiB] реализация OpenSSL работает лучше, чем встроенная
  • Я знаю, что результатов слишком мало, чтобы претендовать на закономерность, но похоже, что обе реализации линейно пропорциональны (с точки зрения времени) размеру сообщения (но встроенный наклон кажется немного круче, что означает, что он будет работать хуже). в долгосрочной перспективе)

В заключение, если все ваши сообщения небольшие, а встроенная реализация работает лучше всего для вас, то используйте ее.



Обновление #1

Графическое представление (мне пришлось уменьшить количество итераций timeit на порядок, так как для больших сообщений это занимало бы слишком много времени):

Изображение0

и масштабирование области, где пересекаются два графика:

Изображение1

person CristiFati    schedule 17.02.2020
comment
Спасибо за тесты, не могли бы вы добавить для них линейный график? Будет намного проще визуализировать тренд. - person wim; 18.02.2020

Публичные модули Python обычно делегируют методы скрытому модулю.

Например, полный код модуля collections.abc:

from _collections_abc import *
from _collections_abc import __all__

Функции hashlib создаются динамически:

for __func_name in __always_supported:
    # try them all, some may not work due to the OpenSSL
    # version not supporting that algorithm.
    try:
        globals()[__func_name] = __get_hash(__func_name)

Определение always_supported таково:

__always_supported = ('md5', 'sha1', 'sha224', 'sha256', 'sha384', 'sha512',
                      'blake2b', 'blake2s',
                      'sha3_224', 'sha3_256', 'sha3_384', 'sha3_512',
                      'shake_128', 'shake_256')

И get_hash либо __get_openssl_constructor, либо __get_builtin_constructor:

try:
    import _hashlib
    new = __hash_new
    __get_hash = __get_openssl_constructor
    algorithms_available = algorithms_available.union(
            _hashlib.openssl_md_meth_names)
except ImportError:
    new = __py_new
    __get_hash = __get_builtin_constructor

__get_builtin_constructor является запасным вариантом для (снова) скрытого модуля _hashlib:

def __get_openssl_constructor(name):
    if name in __block_openssl_constructor:
        # Prefer our blake2 and sha3 implementation.
        return __get_builtin_constructor(name)
    try:
        f = getattr(_hashlib, 'openssl_' + name)
        # Allow the C module to raise ValueError.  The function will be
        # defined but the hash not actually available thanks to OpenSSL.
        f()
        # Use the C function directly (very fast)
        return f
    except (AttributeError, ValueError):
        return __get_builtin_constructor(name)

Выше в коде hashlib у вас есть это:

def __get_builtin_constructor(name):
    cache = __builtin_constructor_cache
    ...
    elif name in {'MD5', 'md5'}:
        import _md5
        cache['MD5'] = cache['md5'] = _md5.md5

Но md5 нет в __block_openssl_constructor, поэтому версия _hashlib/openssl предпочтительнее версии _md5/builtin:

Подтверждение в REPL:

>>> hashlib.md5
<built-in function openssl_md5>
>>> _md5.md5
<built-in function md5>

Эти функции являются различными реализациями алгоритма MD5, а openssl_md5 вызывает динамическую системную библиотеку. Вот почему у вас есть некоторые изменения производительности. Первая версия определена в https://github.com/python/cpython/blob/master/Modules/_hashopenssl.c и другой в https://github.com/python/cpython/blob/master/Modules/md5module.c, если вы хотите проверить различия.

Тогда почему функция _md5.md5 определена, но никогда не используется? Я предполагаю, что идея состоит в том, чтобы гарантировать, что некоторые алгоритмы всегда доступны, даже если openssl отсутствует:

В этом модуле всегда присутствуют конструкторы алгоритмов хэширования: sha1(), sha224(), sha256(), sha384(), sha512(), blake2b() и blake2s(). (https://docs.python.org/3/library/hashlib.html)

person jferard    schedule 11.02.2020
comment
Привет, jferard, 90% ответа касается низкоуровневых деталей того, как hashlib.md5 будет привязан либо к встроенной реализации md5, либо к реализации openssl_md5. Но механика реализации этих импортов/откатов — скучные детали (я тоже могу прочитать об этом в исходниках). Для этой награды меня больше интересует ответ, который объясняет, чем отличаются openssl и встроенная реализация md5, и почему явно более быстрая встроенная версия не выбрана по умолчанию. Была ли реализация openssl в чем-то лучше? Почему? - person wim; 12.02.2020
comment
Можете ли вы добавить профилирование доказательств того, что разница в производительности связана с накладными расходами при вызове динамической системной библиотеки? - person wim; 12.02.2020

Моя теория, основанная на поиске на bugs.python.org и чтении истории коммитов cpython git:

cpython перешел на openssl md5 в 2005 году, потому что он был быстрее, чем встроенная реализация. В 2007 году они добавили новую встроенную реализацию, которая работает быстрее, чем openssl, но никогда не переключались обратно. Оба эти изменения были сделаны Грегори П. Смитом.

Вот мои доказательства.

  • В 2005 году Грег создал проблему bpo "модули sha и md5 должны использовать OpenSSL, когда это возможно". Это изменение сделано в этой фиксации.
  • В 2007 году Грег добавил новый быстрый модуль md5 в эту фиксацию.
  • Реализация _md5 в Python 3.8 практически такая же (я смотрю на коммит ea316fd21527)

Я думаю, что сопровождающие cpython, вероятно, будут готовы вернуться к _md5, когда он будет доступен, поскольку уже не правда, что реализация openssl быстрее (и, возможно, это было неправдой в течение последних 13 лет).

person Michael Graczyk    schedule 17.02.2020
comment
Похоже, что реализация openssl на самом деле работает лучше для сообщений, которые с большей вероятностью будут использоваться на практике (например, файлы размером порядка килобайт или больше). См. другой ответ об этом. - person wim; 18.02.2020