[О блоге]
[наверх]
[пред]
[2023-10-08 11:34:49+03:00]
[0e2b923312e248cdf94178d84d9c4a5831cca9f2]
goredo 2.0
http://www.goredo.cypherpunks.ru/News.html#Release-2_005f0_005f0
После более чем недели разработки по вечерам, выпустил новую версию
сабжа. В рассылке сказали что на большом количестве целей goredo
работает в разы медленнее чем apenwarr/redo. Я особо с оптимизацией и не
заморачивался, но, действительно, время затрачиваемое на нахождение
зависимостей в нём идёт на минуты, а redo-sources я вообще не смог
дождаться на своём синтетическом примере.
Оказалось, что в нём просто тьма одной и той же работы выполнялась
каждый раз при рекурсивном прохождении информации о зависимостях.
redo-sources был написал просто ужасно неоптимально, слишком дуболомно.
Очень много времени затрачивалось на постоянную работу с путями:
path.Split, path.Join, path.Abs и подобные. Сотни мегабайт памяти
уходило на хранение строчек, большая часть из которых одинакова.
Информация о зависимостях в памяти хранилась вообще как буквально
map[string]string, как словарик вытащенный о каждой из цели из .rec
файла, где у всех одни и те же строковые ключи.
Оптимизацию и проверку скорости омрачал тот факт, что регулярно стали
появляться ошибки при любых операциях с файлами, типа fd.Stat(),
fd.Read(). Раньше никогда не вылазило ничего подобное. Но когда
запускаются десятки тысяч целей, у каждой из которых по тысяче
зависимостей, то иногда секунд десять может всё идти нормально, иногда
чуть ли не секунду от каждой цели вылазит сообщение об ошибке работы с
файлом. Долго выяснял, но стало похоже на то, что как-будто os.File
пытается работать с уже другим файловым дескриптором. Как-будто один
закрыли и его переиспользовали, но os.File об этом не знал.
Оказалось, что в Go на os.File вешается finalizer, который закрывает
файловый дескриптор. А срабатывать он будет если попадает под сборщик
мусора. Долго искал и место где у меня с какой-то стати, но переменная
выходит за scope и почему-то попадает под GC. Чуть ли не в самом начале
main(), действительно, был if {} scope внутри которого открывался файл,
и не предполагалось что он должен закрываться. Короче исправил проблему.
Вместо чисто string путей использовал структуру, внутри которой
предвычисленные пути (абсолютный, путь к .rec файлу зависимости,
относительный путь к Cwd). А также завёл кэш этих самых строчек, чтобы
не создавалась куча копий одной и той же строки по сути.
Вместо хранение map[string]string зависимости, имело смысл хранить
нормальные структуры с int-ами. Так и сделал. Даже хэш у меня прежде
хранился в виде шестнадцатеричной строки, ужас. Но потом до меня дошло,
что информация об иноде: размер, номер иноды, ctime, mtime -- не требуют
явного парсинга, ибо они используются по сути просто только для
сравнения. Всё это можно просто положить одной бинарной строкой
сконкатенированной. Завёл кэш и инодов, чтобы не плодилось копий, и кэш
хэшей. Позже дошло что вероятность того, что для заданной иноды хэш
может отличаться -- мизерная. Поэтому можно иноду и хэш хранить вместе в
одной 6*8+32 байта строчке. А ещё можно использовать не строчку, которая
хранит указатель и длину данных, а просто массив фиксированного размера.
Ну и дошло до того, что парсинг двух гигабайт recfile-ов отнимает (что
не удивительно) львиную долю времени. Перешёл на бинарный формат. Думал
было взять что-то существующее, типа Protocol Buffers, но плюнул. Просто
друг за другом идущие chunk-и, каждый из которых начинается на 16-бит
длину, далее один байт тип (ifcreate, ifchange, ifchange-nonexistent,
stamp, always), а далее либо имя файла (до конца chunk), либо перед ним
ещё фиксированного размера поля иноды с хэшом. redo-depfix команда
теперь может сконвертировать все имеющиеся .redo/*.rec в .dep бинарные
файлы, а redo-dep2rec может наоборот показать .dep в виде recfile.
В общем, теперь всё в разы быстрее apenwarr/redo, на порядки чем было
прежде. Правда и ценой использования RAM.
[оставить комментарий]