Pull to refresh

Comments 44

Да, pypy любопытен. Но к сожалению, мной не разу не использовался, но при случае обязательно проведу сравнение. Очень интересно на сколько верно то, что говорят о его производительности.
Это примерно так же как с ipy получается — в полтора раза медленнее. Любопытно, спасибо!
А что именно любопытно? Вы ведь тестируете что? Реализацию алгоритма md5 написанную на С? Ну да, этот кусок в pypy менее вылизан чем в cpython. Так он же и не для этого.
Любопытно скорее то, что тот диалект, на котором написан pypy (RPython) примерно эквивалентен по производительности с C# на котором написан ipy.
Очевидный ход — код в строке 7 md5 = MD5CryptoServiceProvider() лучше бы вынести в глобальную переменную, чтобы не создавать новый объект на каждой итерации.
Аналогично в строках 9-12 лучше использовать StringBuilder вместо создания нового объекта строки на каждый байт файла:

 9|      result = StringBuilder()
10|      for b in hash:
11|          result.Append(b.ToString("x2"))
12|      return result.ToString()
так же мало повлияло на результат:
0:00:00.161000
0:00:00.091000
0:00:00.094000
0:00:00.098000
0:00:00.096000
0:00:00.096000
0:00:00.098000
0:00:00.097000
0:00:00.100000
0:00:00.096000
При размерах строк до мегабайта — стрингбилдер медленнее и менее эффективен, чем простая конкатенация
А разве StringBuilder не копирует область памяти занимаемой добавляемой строкой в заранее выделенную память?
Ну и мало влияющая на скорость доработка:

output += fname.replace(rootpath, '', 1) + ':' + md5sum + '\n'
можно заменить на
output += Path.GetFileName(fname) + ':' + md5sum + '\n'

Если будут проблемы с окончаниями строк (в винде используется \r\n), вместо \n можно использовать Environment.NewLine
output += Path.GetFileName(fname) + ':' + md5sum + '\n'
не подойдёт, так как возвращает только имя файла, тут же нужно относительный путь от корневой директории исключая её саму. То есть, если файл лежит где-то в /home/username/project/app/lib/file.py должно вернуться /lib/file.py чтобы удобно можно было потом на стороне клиента сравнить с таким же файлом не затрагивая текущую директорию, которая потенциально может отличаться.

В целом, благодарю за советы, пожалуй, так правильнее, хоть и не влияет на скорость.
Совершенно не влияет. По крайней мере в таком масштабе. Хотя, понятно, что в теории должно.
Тот же замер, во второй колонке md5 = MD5CryptoServiceProvider() вынесена в глобальную область
0:00:00.174000 vs 0:00:00.172000
0:00:00.092000 vs 0:00:00.100000
0:00:00.097000 vs 0:00:00.100000
0:00:00.096000 vs 0:00:00.094000
0:00:00.103000 vs 0:00:00.096000
0:00:00.102000 vs 0:00:00.102000
0:00:00.104000 vs 0:00:00.097000
0:00:00.098000 vs 0:00:00.097000
0:00:00.095000 vs 0:00:00.095000
0:00:00.096000 vs 0:00:00.095000
Слишком малое время. В пределах погрешности. Нужно запускать так, чтобы выполнялось не меньше секунды (а лучше 10).
Да, масштаб времени не самый удачный. Но сравнить конкретно эти три случая вполне позволяет. Не позволяет, возможно, заметить более тонкую оптимизацию, которую предлагает catlion, но там по всей видимости речь уже будет идти о микросекундах. В данном случае они не сильно показательны. Если бы разница была бы не в сотых долях секунды, а хотя бы в тысячных — был бы смысл.
Для начала я бы попробовал посмотреть, как это будет работать на среднем клиентском железе.

Ну и чтобы два раза не вставать, если один из файлов будет открыт на запись, или удален в промежутке между 17 строкой и соответствующей итерацией, ваш код упадет с неотловленным исключением.
С исключением-то можно и по простому, добавить try-except-else в функцию:

def getMD5sum(fileName):
    try:
        b = System.IO.File.ReadAllBytes(fileName)
    except System.IO.FileNotFoundException:
        print 'file ' + fileName + ' deleted'
        result = ''
    except System.IO.IOException:
        print 'file ' + fileName + ' is in use'
        result = ''
    else:
        hash = md5.ComputeHash(b)
        hashStr = StringBuilder()
        for b in hash:
            hashStr.Append(b.ToString("x2"))
        result = hashStr.ToString()
    return result

Естественно, заменив print'ы на запись в лог.
Ну, а на счёт тестирования на типовом клиентском железе — это конечно же необходимо и обязательно. Нужно будет развернуть несколько виртуалок с разными характеристиками и версиями Windows и погонять там.
Заголовок правильнее было бы назвать «CPython vs. IronPython»
С учётом того, что речь действительно идет о разных интерпретаторах одного языка — логично писать CPython. Подправил.
Оба варианта доставят массу неприятностей если дать им пройтись по коллекции HD-видео, ISO-образов и прочих гигабайтных файлов: именно для таких случаев и существует метод update.
В первом варианте он и используется. Кроме того, автор написал, что у него файлы до 3-х мегабайт. А если нужен хеш больших файлов, можно в цикле читать частями и делать update: fileName.read(blockSize)
в случае автора он избыточен, но я просто предупредил на случай запуска скрипта по более «тяжелым» файлам =)
Пока не пробовал. Но как я понял, глянув сейчас в поиске, оно способно обеспечить более быстрый запуск, что может оказаться полезным.
Можно написать md5 функцию в одну строку:
def getMD5sum(fileName): return hashlib.md5(open(fileName, 'rb').read()).hexdigest()

Вместо replace хорошо-бы использовать slices:
output+='{0}:{1}\n'.format(fname[len(rootpath):], md5sum)

Еще мне не нравится, что вы используете os.walk(), который разбивает имя файла на части, а потом назад его собираете. Но я не нашел другого способа рекурсивно получить все файлы.

Это все, конечно, не влияет на производительность. Кстати, если поменять алгоритм на CRC32, можно получить выигрыш в 30%.
output+='{0}:{1}\n'.format(fname[len(rootpath):], md5sum)
Еще мне не нравится, что вы используете os.walk(), который разбивает имя файла на части, а потом назад его собираете. Но я не нашел другого способа рекурсивно получить все файлы.

os.walk основывается на os.listdir, который оперирует понятиями root path и basename — отсюда и разделение полного пути файла/директории на части.
Упс, последняя строка должна быть return zlib.crc32(open(fileName, 'rb').read())
CRC32 стоит попробовать, спасибо за мысль
Я тут подумал, а зачем открывать файлы? Если делать хеш со строки, получается примерно в 200 раз быстрее.
def getMD5sum(fileName): return hashlib.md5(fileName).hexdigest()
Уже понял зачем. Совсем забыл для чего предназначен скрипт.
С другой стороны, можно к названию добавлять размер файла, например.
При обновлении файла могут быть коллизии с одинаковым размером
Почему вы не храните где-нибудь номер версии? Зачем такие сложности с вычислением хешей?
С хлещем проще. Собрал новую сборку, прогнал скрипт и все. С версиями пришлось бы контролировать процесс более тщательно для каждого отдельного файла. К тому же, как видите, скрипты сравнительно малы и просты в обоих реализациях. Опять же, в конце концов, на клиенте можно схалтурить и хеш не вычислять или вычислять в случае какой-то особой необходимости. Вместо этого просто сохранять полученный при прошлом обновлении файл с хешами и сравнивать эталон с ним. Хотя это и не очень правильная мысль.
С хлещем = С хешем. Автозамена неудачно сработала
Предлагаю использовать следующий вариант адаптированного для IronPython скрипта. Улучшение производительности, в сравнении с адаптированным вариантом из поста, получилось в ~3-4 раза на наборе из 5227 файлов, общим размером в 381Мб. Правда результат немного отличается, от результата скрипта из поста — путь начинается с ".", а не со "/", и под виндой будут виндовые слеши, но это вроде некритично. Дополнительно можно немного ускорить скрипт, добавив еще одно некритическое различие, удалив вызов ToLower() для хеша. Преимуществом скрипта ниже, кроме скорости исполнения, является и бережное отношение к памяти, т.е. для подсчета хеша, содержимое файла, как и весь список файлов, не загружается полностью, вывод результата не аккумулируется в программе.
Буду признателен автору, если он измерит производительность этого скрипта на своей машине и своем наборе тестовых файлов.

Кодярник
from System.IO import StreamWriter, Directory, SearchOption, File, Path
from System import String, BitConverter, Environment, Array
from System.Security.Cryptography import MD5CryptoServiceProvider
from System.Diagnostics import Stopwatch

sw = Stopwatch.StartNew()

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

#hack to get rid of string replace in output
Environment.CurrentDirectory = rootpath

appFiles = Directory.EnumerateFiles('.', '*', SearchOption.AllDirectories)

output = StreamWriter(File.OpenWrite(Path.Combine(workingDir, 'checksums-fastest.csv')))
#magically enumerate some how speedup loop, probably .net -> python iterators interop flavor
for _, file in enumerate(appFiles):
    output.Write(file)
    output.Write(":")
    output.WriteLine(getMD5sum(file))

output.Close()

Environment.CurrentDirectory = workingDir

print sw.Elapsed

Ух как. Вот этот результат уже очень даже интересен.
00:00:00.1086513
00:00:00.0653087
00:00:00.0619235
00:00:00.0580854
00:00:00.0581689
00:00:00.0563488
00:00:00.0576192
00:00:00.0562254
00:00:00.0565140
00:00:00.0569575
совершенно эквивалентен по скорости полученному в CPython

Немного его переделал:
заменил output.Write(file) на output.Write(file.replace(".", "", 1).replace("\\", "/"))
время замерил с помощью питоньего инструментария
(чтобы мерить одним методом одинаковый функционал с одинаковыми входными и выходными данными)
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

Немного медленнее, но не раздражает. С учётом того, что в реальности время мериться не будет, соответственно и ресурсы на это тратиться не будут. Уйдет лишний импорт.
Кстати, попробовал в вашем варианте вставить вместо File.OpenRead(fileName) File.ReadAllBytes(fileName) и сразу получил
0:00:00.113000
0:00:00.082000
0:00:00.078000
0:00:00.078000
0:00:00.077000
0:00:00.079000
0:00:00.081000
0:00:00.080000
0:00:00.078000
0:00:00.079000

Не уж-то на столько медленнее? Будет хорошим поводом пройтись по остальному коду приложения…
Спасибо!
File.ReadAllBytes(fileName) плох тем, что если наткнется на большой файлик, пямять приложения будет расходоваться не очень рационально. Кроме того, ComputeHash, вычисляющийся по System.IO.Stream, работает в фиксированном объеме памяти — 4Кб, т.е. буфер выделяется один раз и небольшого размера.
UFO just landed and posted this here
Просто привычка, чтобы лишний раз файл не блокировать. Тут это не нужно и Ваш вариант должен быть эффективнее с точки зрения потребления памяти
UFO just landed and posted this here
Sign up to leave a comment.

Articles