Python vs. IronPython: вычисление MD5-хеша

в 4:44, , рубрики: .net, ironpython, md5, python, Песочница, производительность, метки: , , ,

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

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

Но тут обнаружилась маленькая деталь. В IronPython скрипт выполнялся в несколько раз медленнее. И это на достаточно быстром железе. У пользователя же оно могло быть значительно слабее. Началась оптимизация, в ходе которой родилась мысль провести сравнение производительности Python и IronPython на этом примере. В статье, соответственно, рассматриваются три отдельных результата: для Python, IronPython и IronPython с адаптированным скриптом.
Результаты под катом.

Конфигурация

  • Core i5 650 3.20 GHz
  • 8 Гб ОЗУ
  • Windows 7 Enterprise 64x
  • Python 2.7.1
  • IronPython 2.7.3

В качестве «еды» для скрипта использовалась директория с файлами приложения. В её состав входит Runtime самого 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, я не смог увидеть разницы, превышающей статистическую погрешность.

Результаты

И так, результаты холодных пусков (средние):

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

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

Есть ещё один нюанс: на клиенте данный скрипт должен быть встроен в само приложение. Соответственно, интересует не столько время холодного запуска, сколько время его непосредственного исполнения, без учёта старта интерпретатора. Воспроизведём такое поведение, поместив код в цикл.

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

Python 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. За это время скрипт, запущенный в нативном python уже успевает завершиться. Python стартует фактически мгновенно и как видно, первая итерация была такой же быстрой, как и последующие. При этом видно, что даже оптимизированный для ipy код, запущенный на горячую – почти в полтора раза медленнее нативного.

Использование одинакового кода для Python и IronPython и вовсе выглядит малопригодным в случае, если производительность сколько-нибудь критична. Впрочем, это не единственное ограничение IronPython при использовании одного и того же кода. Там есть кое-какие нюансы и баги не касающиеся производительности, но это уже выходит за рамки данной статьи. Впрочем, хочу оговориться, что об отказе использования IronPython речи также не идёт. Он вполне успешно справляется с возложенными на него обязанностями.

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

Автор: Juralis

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js