Понадобилось как-то в проекте сделать автообновление для клиентского приложения. Так как работало оно с отечественными криптопровайдерами, доступ к которым проще получить из .Net, написано оно было на IronPython. При этом C# выбран не был, так как на стороне сервера уже активно использовался python и сильно переучиваться не хотелось.
Казалось бы всё просто. Был набросан скрипт, который вычисляет md5-хеши для файлов входящих в состав приложения, сводит всё в один файл со строками вида “относительный путь”:”md5” и выкладывает в директорию раздачи статики nginx. Клиентское приложение при запуске забирает файлик, прогоняет аналогичный скрипт, и сверяет полученный результат с эталоном.
Но тут обнаружилась маленькая деталь. В IronPython скрипт выполнялся в несколько раз медленнее. И это на достаточно быстром железе. У пользователя же оно могло быть значительно слабее. Началась оптимизация, в ходе которой родилась мысль провести сравнение производительности CPython и IronPython на этом примере. В статье, соответственно, рассматриваются три отдельных результата: для CPython, IronPython и IronPython с адаптированным скриптом.
Результаты под катом.
В качестве «еды» для скрипта использовалась директория с файлами приложения. В её состав входит Runtime самого IronPython, дополнительные библиотеки и прочие необходимые файлы. Всего порядка 350 файлов от килобайта до трех мегабайт.
Код скрипта:
Тот же скрипт, адаптированный для IronPython:
В принципе, вся адаптация сводится к тому, что чтение/запись файлов и вычисление хешей переписаны на .Net. Это даёт достаточный прирост производительности. Связано это с тем, что сам ipy написан на c# и большая часть «батареек» просто обёртка к .Net. В этом смысле интересным может выглядеть разница между 19 строкой основного и 21 адаптированного:
В ipy второй вариант оказался быстрее. Что касается python, я не смог увидеть разницы, превышающей статистическую погрешность.
И так, результаты холодных пусков (средние):
Не вооруженным глазом видно, что один и тот же скрипт в python и IronPython исполняются с более чем пятикратным преимуществом на стороне python. В тоже время, скрипт, адаптированный для ipy хоть и исполняется по-прежнему медленнее, но результат уже вполне приемлем.
Есть ещё один нюанс: на клиенте данный скрипт должен быть встроен в само приложение. Соответственно, интересует не столько время холодного запуска, сколько время его непосредственного исполнения, без учёта старта интерпретатора. Воспроизведём такое поведение, поместив код в цикл.
Типичные результаты:
По результатам этого теста уже можно сделать более или менее правдоподобный вывод. Видно, что приблизительно 0,7 секунды – время, необходимое просто для запуска самого интерпретатора IronPython. За это время скрипт, запущенный в нативном python уже успевает завершиться. CPython стартует фактически мгновенно и как видно, первая итерация была такой же быстрой, как и последующие. При этом видно, что даже оптимизированный для ipy код, запущенный на горячую – почти в полтора раза медленнее нативного.
Использование одинакового кода для CPython и IronPython и вовсе выглядит малопригодным в случае, если производительность сколько-нибудь критична. Впрочем, это не единственное ограничение IronPython при использовании одного и того же кода. Там есть кое-какие нюансы и баги не касающиеся производительности, но это уже выходит за рамки данной статьи. Впрочем, хочу оговориться, что об отказе использования IronPython речи также не идёт. Он вполне успешно справляется с возложенными на него обязанностями.
Рад буду услышать конструктивную критику.
UPD
mstyura предложил более оптимизированный вариант скрипта для ipy с более интересным результатом:
Результат этого варианта:
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() скорость сильно не меняется
Казалось бы всё просто. Был набросан скрипт, который вычисляет 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() скорость сильно не меняется