Cpython Против Ironpython: Вычисление Хеша Md5

Как-то мне нужно было автоматически обновить клиентское приложение в проекте.

Поскольку он работал с отечественными поставщиками криптовалют, к которым проще получить доступ из .

Net, он был написан на IronPython. При этом C# не был выбран, так как Python уже активно использовался на серверной стороне и мне не хотелось много переучиваться.

Казалось бы, все просто.

Был набросан скрипт, который вычисляет md5-хеши для файлов, входящих в приложение, объединяет все в один файл со строками типа «относительный путь»: «md5» и помещает в статический каталог дистрибутива nginx. Клиентское приложение при запуске берет файл, запускает аналогичный скрипт и сравнивает результат с эталоном.

Но тут всплыла маленькая деталь.

В IronPython скрипт работал в несколько раз медленнее.

И это на достаточно быстром железе.

Для пользователя это может быть гораздо слабее.

Началась оптимизация, в ходе которой родилась идея сравнить производительность CPython и IronPython на этом примере.

В статье соответственно рассматриваются три отдельных результата: для CPython, IronPython и IronPython с адаптированным скриптом.

Результаты под катом.



Конфигурация
  • Core i5 650 3,20 ГГц
  • 8 ГБ ОЗУ
  • Windows 7 Корпоративная x64
  • Питон 2.7.1
  • ЖелезоПитон 2.7.3
В качестве «питания» для скрипта использовался каталог с файлами приложения.

Он включает в себя саму среду выполнения IronPython, дополнительные библиотеки и другие необходимые файлы.

Всего имеется около 350 файлов от килобайта до трёх мегабайт. Код скрипта:

  
  
  
  
   

1| import os 2| import hashlib 3| 4| def getMD5sum(fileName): 5| m = hashlib.md5() 6| fd = open(fileName, 'rb') 7| b = fd.read() 8| m.update(b) 9| fd.close() 10| return m.hexdigest() 11| 12| output = '' 13| rootpath = 'app' 14| 15| for dirname, dirnames, filenames in os.walk(rootpath): 16| for filename in filenames: 17| fname = os.path.join(dirname, filename).

replace('\\', '/') 18| md5sum = getMD5sum(fname) 19| output+='{0}:{1}\n'.

format(fname.replace(rootpath, ''), md5sum) 20| 21| f = open('.

/checksums.csv', 'w') 22| f.write(output) 23| f.close()

Тот же скрипт, адаптированный для IronPython:

1| import os 2| import System.IO 3| from System.Security.Cryptography import MD5CryptoServiceProvider 4| 5| def getMD5sum(fileName): 6| b = System.IO.File.ReadAllBytes(fileName) 7| md5 = MD5CryptoServiceProvider() 8| hash = md5.ComputeHash(b) 9| result = '' 10| for b in hash: 11| result += b.ToString("x2") 12| return result 13| 14| output = '' 15| rootpath = 'app' 16| 17| for dirname, dirnames, filenames in os.walk(rootpath): 18| for filename in filenames: 19| fname = os.path.join(dirname, filename).

replace('\\', '/') 20| md5sum = getMD5sum(fname) 21| output += fname.replace(rootpath, '', 1) + ':' + md5sum + '\n' 21| 22| System.IO.File.WriteAllText('checksums.csv', output)

В принципе, вся адаптация сводится к тому, что чтение/запись файлов и расчет хешей переписаны в .

Net. Это дает достаточный прирост производительности.

Это связано с тем, что сам ipy написан на C# и большая часть «батарейок» — это всего лишь обертка для .

Net. В этом смысле интересно может выглядеть разница между строкой 19 основной и строкой 21 адаптированной:

19| output += '{0}:{1}\n'.

format(fname.replace(rootpath, ''), md5sum)



21| output += fname.replace(rootpath, '', 1) + ':' + md5sum + '\n'

В ipy второй вариант оказался быстрее.

Что касается Python, я не увидел разницы, кроме статистической погрешности.



Результаты
И так, результаты холодных запусков (средние):
  • CPython: ~0,06 с.

  • IronPython: ~0,33 с.

  • IronPython (адаптированный скрипт): ~0,16 сек.

Невооруженным глазом видно, что один и тот же скрипт на Python и IronPython выполняются с более чем пятикратным преимуществом на стороне Python. При этом, хотя скрипт, адаптированный под ipy, все равно работает медленнее, результат вполне приемлемый.

Есть еще один нюанс: на клиенте этот скрипт должен быть встроен в само приложение.

Соответственно, интересует не столько время холодного старта, сколько время его непосредственного выполнения, без учёта запуска интерпретатора.

Давайте воспроизведем это поведение, поместив код в цикл.

Типичные результаты:

CPython ipy ipy (адаптивный)
0:00:00.057000 0:00:00.327000 0:00:00.161000
0:00:00.056000 0:00:00.243000 0:00:00.093000
0:00:00.055000 0:00:00.234000 0:00:00.099000
0:00:00.058000 0:00:00.228000 0:00:00.096000
0:00:00.055000 0:00:00.226000 0:00:00.093000
0:00:00.055000 0:00:00.236000 0:00:00.093000
0:00:00.055000 0:00:00.225000 0:00:00.093000
0:00:00.055000 0:00:00.261000 0:00:00.092000
0:00:00.057000 0:00:00.240000 0:00:00.092000
0:00:00.057000 0:00:00.227000 0:00:00.093000


выводы
По результатам этого теста уже можно сделать более-менее правдоподобный вывод. Видно, что примерно 0,7 секунды — это время, необходимое для простого запуска самого интерпретатора IronPython. За это время скрипт, работающий на нативном питоне, уже успеет завершиться.

CPython запускается практически мгновенно и, как видите, первая итерация прошла так же быстро, как и последующие.

При этом видно, что даже оптимизированный под ipy код, запущенный в горячем виде, почти в полтора раза медленнее нативного.

Использование одного и того же кода для CPython и IronPython кажется совершенно бесполезным, если производительность вообще критична.

Однако это не единственное ограничение IronPython при использовании одного и того же кода.

Есть некоторые нюансы и баги, не связанные с производительностью, но это выходит за рамки данной статьи.

Однако хочу оговориться, что об отказе от использования IronPython также речи не идет. Он вполне успешно справляется с возложенными на него обязанностями.

Буду рад услышать конструктивную критику.

УПД Мстюра предложил более оптимизированную версию скрипта для ipy с более интересным результатом:

from System.IO import StreamWriter, Directory, SearchOption, File, Path from System import String, BitConverter, Environment, Array from System.Security.Cryptography import MD5CryptoServiceProvider def getMD5sum(fileName): stm = File.OpenRead(fileName) md5 = MD5CryptoServiceProvider() hash = md5.ComputeHash(stm) stm.Close() return BitConverter.ToString(hash).

Replace("-", "").

ToLower() rootpath = 'app' workingDir = Environment.CurrentDirectory Environment.CurrentDirectory = rootpath appFiles = Directory.EnumerateFiles('.

', '*', SearchOption.AllDirectories) output = StreamWriter(File.OpenWrite(Path.Combine(workingDir, 'checksums.csv'))) for _, file in enumerate(appFiles): output.Write(file.replace(".

", "", 1).

replace("\\", "/")) output.Write(":") output.WriteLine(getMD5sum(file)) output.Close() Environment.CurrentDirectory = workingDir

Результат этого варианта: 0:00:00.116000 0:00:00.063000 0:00:00.064000 0:00:00.063000 0:00:00.059000 0:00:00.059000 0:00:00.058000 0:00:00.058000 0:00:00.058000 0:00:00.059000 Видно, еще немного и он превзойдет питон — они почти на одном уровне.

Старт конечно по-прежнему медленный, но и стал быстрее, видимо из-за того, что библиотеки Python не импортируются и не используются.

Но если вы просто добавите import os и вызовете os.walk(rootpath) один раз во время простоя, это увеличит время первой итерации до ~0,145 с! Однако, видимо, так сложна сама функция.

Если вы вызовете что-то простое, например os.getcwd(), скорость не сильно изменится.

Теги: #python #IronPython #md5 #производительность #python #.

NET

Вместе с данным постом часто просматривают: