Python
.NET
15 August 2012

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

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

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

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

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

  • Core i5 650 3.20 GHz
  • 8 Гб ОЗУ
  • Windows 7 Enterprise x64
  • 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, я не смог увидеть разницы, превышающей статистическую погрешность.

Результаты

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

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

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

UPD
mstyura предложил более оптимизированный вариант скрипта для 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 — идут практически наравне. Старт конечно всё ещё медленный, но и он стал быстрее, по всей видимости за счёт того, что не импортируются и не используются библиотеки Python. Но если сделать только добавить import os и в холостую однократно вызвать os.walk(rootpath) — это увеличит время первой итерации до ~ 0.145 c! Впрочем, видимо это сама функция так тяжела. Если вызвать что-нибудь простое типа os.getcwd() скорость сильно не меняется

+22
9.3k 41
Comments 44