После реализации в LazDiscret2 возможности расчета динамических режимов, мне захотелось сравнить достигнутые результаты с какой-либо из инкарнаций SPICE. Под рукой оказалась SpiceOpus (ver. 2.31 Rev. 180 http://www.spiceopus.si/), которая базируется на SPICE 3f4 (patched to 3f5). Сравнение выполнялось по результатам анализа переходного процесса для одного из режимов цепи, описанной в файле 02_xform (Nonlinear transformer), который входит набор примеров, распространяемых в пакете инсталляции SpiceOpus.
В примере выполняется выполняется расчет переходного процесса при работе нелинейного трансформатора, нагруженного на диодный мост с RC нагрузкой, частота источника. Характеристика намагничивания сердечника трансформатора задана в виде аналитического выражения, время окончания расчета было равно 15 секундам. Всего при расчете выполнялось свыше 200000 шагов интегрирования. Если численные результаты расчета SpiceOpus и LazDiscret2 полностью совпадали, то с временем расчета ситуация была несколько другая. У SpiceOpus расчет выполнялся около 4-х секунд, а у LazDiscret2 - около 6 секунд, то есть в полтора раза дольше.
Естественно, пришлось заняться оптимизацией процедур расчета динамических режимов. И вот тут то я обнаружил, что использование цикла FOR … IN … DO может привести к увеличению длительности расчета.
Как известно* в Free Pascal два вида циклов FOR:
- 1. FOR control_var := initial_value TO(DOWNTO) final_value DO statement;
- 2. FOR control_var IN enumerable DO statement.
Одним из случаев, когда может применяться вторая форма, является работа с перечислимыми (enumerable) классами, которые поддерживают интерфейсы IEnumerator и IEnumerable.
В модуле Classes такими классами являются:
- TFPList — перечисляет все указатели (pointers) в списке.
- TList — перечисляет все указатели (pointers) в списке.
- TCollection — перечисляет все члены (items) в коллекции.
- TStrings — перечисляет все строки в списке.
- TComponent — перечисляет все дочерние компоненты, принадлежащие компоненту.
Как любитель попробовать новые конструкции в программировании, я реализовал как перечислимые практически все списки для классов моделей элементов цепи в LazDiscret2 и применял цикл FOR … IN … DO при необходимости выполнения операций над всеми элементами цепи.
Для оценки было выполнено тестовое сравнение длительности следующих двух циклов:
1) |
for i := 0 to list.Count-1 do
PInteger(list[i])^ := 1; |
|
2) |
for pitem in list do //var pitem: PInteger;
pitem^ := 1; |
Каждый из этих циклов выполнялся внутри другого цикла с большим числом повторений.
Оказалось, что выполнение второго цикла требует примерно в 2,4 раза больше времени, чем выполнение первого цикла. Это несмотря на то, что оба цикла реализуют абсолютно одинаковые операции.
Почему это происходит легко выяснилось из анализа ассемблерных листингов обоих циклов, которые были получены при компиляции с опцией -al, уровень оптимизации принимался -O2.
1) Цикл FOR … TO … DO
for i := 0 to list.Count-1 do
PInteger(list[i])^ := 1;
# [80] for i := 0 to list.Count-1 do
movl U_$P$TEST_FOR_$$_LIST,%eax //list -> eax - загрузка self
call CLASSES$_$TLIST_$__$$_GETCOUNT$$LONGINT //call list.Count
leal -1(%eax),%ebx //(list.Count-1) -> ebx - final_value
movl $0,U_$P$TEST_FOR_$$_I //0 -> i - initial_value
cmpl U_$P$TEST_FOR_$$_I,%ebx //compare i & final_value
jl .Lj122 //i > final_value goto end_loop - skip loop
subl $1,U_$P$TEST_FOR_$$_I //decrement i
.Lj123:
//start loop
addl $1,U_$P$TEST_FOR_$$_I //increment i
# [81] PInteger(list[i])^ := 1;
movl U_$P$TEST_FOR_$$_I,%edx //i -> edx
movl U_$P$TEST_FOR_$$_LIST,%eax //list -> eax - загрузка self
call CLASSES$_$TLIST_$__$$_GET$LONGINT$$POINTER //call list.Get(i)
movl $1,(%eax) //1 -> (eax)
cmpl U_$P$TEST_FOR_$$_I,%ebx //compare i & final_value
jg .Lj123 //i < final_value goto start_loop
//end_loop
.Lj122:
2) Цикл FOR … IN … DO
for pitem in list do
pitem^ := 1;
# [89] for pitem in list do
//create LISTENUMERATOR
movl U_$P$TEST_FOR_$$_LIST,%eax
call CLASSES$_$TLIST_$__$$_GETENUMERATOR$$TLISTENUMERATOR //call list.GetEnumerator
movl %eax,%ebx //LISTENUMERATOR -> ebx
testl %ebx,%ebx //check ebx == nil
je .Lj180 //ebx == nil - skip loop
//TRY -> push in ExceptAddrstack LongJump data buffer
//Function fpc_PushExceptAddr (Ft: Longint;_buf,_newaddr : pointer): PJmp_buf ;
movl $1,%eax //Ft=1
leal -120(%ebp),%edx //_buf
leal -96(%ebp),%ecx //_newbuf
call FPC_PUSHEXCEPTADDR
//установка LongJump data
//Function fpc_SetJmp (Var S : Jmp_buf) : longint
//при нормальном завершении fpc_SetJmp возвращает 0
//при возврате после Exception fpc_SetJmp возвращает 1
call FPC_SETJMP //setup longjmp data
pushl %eax //save current eax -> FPC_SETJMP result
testl %eax,%eax //check eax
jne .Lj181 //after exception eax <> 0 -> skip loop
jmp .Lj187
//loop begin --------
.Lj186:
movl %ebx,%eax //listenumerator -> eax
call CLASSES$_$TLISTENUMERATOR_$__$$_GETCURRENT$$POINTER //listenumerator.GetCurrent
movl %eax,U_$P$TEST_FOR_$$_PITEM //save to pitem
# [90] pitem^ := 1; -> loop body
movl $1,(%eax)
.Lj187:
movl %ebx,%eax //listenumerator -> eax
call CLASSES$_$TLISTENUMERATOR_$__$$_MOVENEXT$$BOOLEAN //listenumerator.MoveNext
testb %al,%al //check result MOVENEXT
jne .Lj186 //if result MOVENEXT <> 0 -> next loop
//end loop
.Lj181:
//FINALLY
//убираем свои данные, установленные по fpc_PushExceptAddr
call FPC_POPADDRSTACK //pop ExceptAddrstack
//Destroy listenumerator
movl %ebx,%eax //listenumerator -> eax
movl $1,%edx //1 -> edx
movl (%ebx),%ecx //PVMT -> ecx
call *48(%ecx) //call listenumerator.Destroy
popl %eax //restore eax -> FPC_SETJMP result
testl %eax,%eax //check eax
je .Lj182 //no Exception -> skip FPC_RERAISE
call FPC_RERAISE //повторный вызов Exceptions
.Lj182:
.Lj180:
Выводы, которые можно сделать из анализа листингов:
- 1. В теле цикла FOR … TO … DO выполняется лишь один вызов метода list.Get(i).
- 2. В теле цикла FOR … IN … DO выполняется вызов двух методов listenumerator.GetCurrent и listenumerator.MoveNext, в которых вызываются методы list.Get(i) и list.Count, соответственно.
- 3. При использовании цикла FOR … IN … DO выполняется неявное создание экземпляра класса listenumerator. Чтобы избежать потерь памяти при возникновении исключений, тело цикла помещается в оператор TRY ... FINALLY. Это приводит к дополнительным затратам времени в случае, когда цикл FOR … IN … DO внутри другого цикла.
В случае LazDiscret2 замена всех FOR … IN … DO на FOR … TO … DO внутри цикла численного интегрирования привело к ощутимому снижению времени анализа динамических режимов. В итоге — с учетом других оптимизаций — удалось уменьшить время анализа описанного выше примера до 2,7 секунд.
* ↑ Michael Van Canneyt. Reference guide for Free Pascal, version 3.0.2. Document version 3.0.2. February, 2017.
|