Главная » Статьи » FreePascal

Особенности применения цикла FOR ... IN ... DO для перечислимых (enumerable) классов

После реализации в 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.

Категория: FreePascal | Добавил: zoleg5763 (20.01.2019)
Просмотров: 250 | Рейтинг: 0.0/0
Всего комментариев: 0
avatar