"Тяжёлые ассерты"
От: Khimik  
Дата: 06.05.19 18:47
Оценка:
В этой теме
Автор: 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, после срабатывания мой ассерт выдаёт сообщение, я могу перейти к коду и дальше продолжить выполнение программы, или шагать дебагером.
"Ты должен сделать добро из зла, потому что его больше не из чего сделать". АБ Стругацкие.
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.