В
этой темеАвтор: Khimik
Дата: 03.01.19
я писал про прямые и обратные задачи в программировании. Например, работа нейросети – это решение прямой задачи, а её обучение – решение обратной. Решение обратной задачи всегда сложнее.
И у меня из этого возникла некая концепция “тяжёлых ассертов”, которую я хочу рассказать.
Как я понимаю, ассертами можно назвать решение прямой задачи, позволяющее на отдельных этапах контролировать правильность решения обратной. Так вот “тяжёлые” ассерты – это такие, которые проверяют всё целиком, но очень тормозят программу, поэтому для их запуска нужен отдельный режим “тяжёлой отладки”.
Мой пример. Есть двумерный динамический массив, непрямоугольный. В нём N элементов; в каждом i-м элементе (от 0 до N-1) есть два массива одинаковой длины (Mi) с интегерами. В этих двух массивах располагаются уникальные индексы: номера от 1 до Mi, без повторов, в разной последовательности. Во втором массиве – обратные индексы: т.е. если в первом, например,
1 3 5 2 4, то во втором
1 4 2 5 3.
В программе периодически меняются числа в одном из этих “спаренных” массивов, и нужно автоматом поменять второй, чтобы сохранилось соответствие. Так вот обычный ассерт – это проверять только i-ю пару, а тяжёлый ассерт – проверять весь массив целиком.
Обычный ассерт быстрее, но тяжёлый проще и универсальнее. Это пример слишком простой, но будет понятнее например так: если имеется не просто двумерный массив, а какой-то сложный иерархический массив, с разными типами данных в каждой ячейке, рекурсией и т.д.; тогда простой ассерт потребуется вставлять в отдельный участок, и вообще этих простых ассертов будет много, а тяжёлый ассерт также будет единственный и надёжный. Только он будет очень тормозить.
Отсюда примерно такой общий подход к оптимизации алгоритма: сначала пишете медленный, но простой и надёжный код, потом делаете его более сложным, и в разных местах вставляете тяжелые ассерты: они должны проверять, что глючный быстрый код работает точно так же, как надёжный медленный.
Вот пример такого кода в моей программе (извиняюсь что стиль сильно нестандартный):
function TLinearTrianlesPlaneUnpackedModel.CalculateQDerivByPointVal(pointx,pointy:integer):double;
var
oldpointval:double;
firstq,altq:double;
result1:double;
function DoCalcQ:double;
begin
result:=CalculateQPart(pointx,pointy);
end;
begin
firstq:=DoCalcQ;
oldpointval:=fpoints[pointy][pointx];
fpoints[pointy][pointx]:=fpoints[pointy][pointx]+valshift;
altq:=DoCalcQ;
result:=(altq-firstq)/valshift;
fpoints[pointy][pointx]:=oldpointval;
if poHeavyDebugMode then begin
result1:=CalculateQDerivByPointVal_simple(pointx,pointy);
if not equal(result,result1) then assert(false);
end;
end;
Это код для МНК-минимизации через градиентный спуск. Для каждого параметра считается градиент; это можно делать "брутфорсово" (поменяли параметр, посмотрели как изменился весь функционал) или более сложно. Код после if poHeavyDebugMode сравнивает результат по брутфорсовому методу и сложному.
Здесь есть момент – фича Delphi (я не знаю, как с этим в других языках). Насколько я знаю, если задать константу poHeavyDebugMode=false, то весь проверочный код станет невидимым для компилятора, т.е. не будет выполняться даже проверка if. По-моему, этой фичи не будет, если константа имеет конкретный тип.
Ещё я перезагрузил стандартную процедуру assert в Delphi, после срабатывания мой ассерт выдаёт сообщение, я могу перейти к коду и дальше продолжить выполнение программы, или шагать дебагером.
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.