From 0e2b923312e248cdf94178d84d9c4a5831cca9f2 Mon Sep 17 00:00:00 2001 From: Sergey Matveev Date: Sun, 8 Oct 2023 11:34:49 +0300 Subject: [PATCH] goredo 2.0 MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit 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. -- 2.50.0