"Тяжёлые ассерты"
От: 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...
Пока на собственное сообщение не было ответов, его можно удалить.