Шаг 8.
Оптимизация с помощью ассемблера.
Написание встраиваемого кода BASM. Оптимизация функций

    На этом шаге мы рассмотрим оптимизацию функций.

    Основная причина использования ассемблера в программах совместно с C++ - повышение производительности. Сначала с помощью ваших собственных текстов или даже лучше, с помощью текстов Turbo Profiler определите функции, выполнение которых отнимает большую часть общего времени выполнения. Оптимизация таких критических функций может значительно повысить производительность вашей программы.


    Замечание. Фредерик П.Брукс в книге The Mythical Man-Month (Мифический человеко-месяц) выдвигает предположение о том, что все проблемы быстродействия можно решить переводом от одного до пяти процентов программ в оптимизированный язык ассемблера. Оптимизация же остальных программ даст незначительные результаты.

    Рассмотрим простой пример - функцию MySquare(), возвращающую квадрат целого значения:

int MySquare(int n)
{
  return n*n;
}  

    Скомпилируем программу с этой функцией с помощью опции -S. В результате получим следующий ассемблерный код:

   push bp                   ;Сохранить регистр указателя базы
   mov bp, sp                ;Адресовать стек регистром bp
   mov bx, word ptr [bp+4]   ;Записать int n в bx
   mov ax, bx                ;Переслать bx в ax
   imul bx                   ;Умножить ax на bx
   jmp short @2@58           ;Перейти к следующей команде
@2@58: 
   pop bp                    ;Восстановить регистр bp
   ret                       ; вернуться в место вызова

    Этот код имеет два очевидных недостатка. Во-первых, целое n (размещенное в стеке по смещению bp+4) записывается в регистр bx и тут же в ax. Почему бы не переслать n непосредственно в регистр ax? Во-вторых, инструкция jmp бессмысленно переходит к следующему же адресу, якобы пропуская код, который, возможно, существовал бы в этом месте при других обстоятельствах.

    Скомпилированный код изобилует подобными недостатками, кажущимися, на первый взгляд, грубыми ошибками, которые компетентный программист, пишущий на ассемблере, никогда бы не допустил. Однако имейте в виду, что если функция MySquare() не обладает статусом критической, эти расточительные инструкции, вероятно, не оказывают ощутимого влияния на быстродействие программы.

    С другой стороны, если функция MySquare() занимает большую часть общего времени выполнения программы, оптимизация функций для достижения максимальной производительности даст приличный выигрыш. Руководствуясь ассемблерным кодом, сгенерированным компилятором, вы можете переписать функцию с использованием операторов asm, как показано в листинге 1.

    Листинг 1. Inline.cpp (оптимизация функции MySquare() с помощью операторов asm)


#include <stdio.h>
int MySquare(int n);
main()
{ int i;
  int array[10];
  for (i=0; i<10; i++)
  array[i]=MySquare(i);
  for (i=0; i<10; i++)
  printf("array[%d]==%d\n", i, array[i]);
  return 0;
}            

//Предотвращение предупреждения "Function should return value"
#pragma warn -rvl
int MySquare (int n)
{ 

/*return n*n; */  
 asm {
         mov ax, word ptr n
         imul bx
        }
}      

// Восстановление предыдущего состояния предупреждения
#pragma warn .rvl
Текст этого приложения можно взять здесь.

    В функции MySquare() первоначальный оператор умножения заключен в скобки комментария. При оптимизации ваших собственных функций никогда не удаляйте первоначальный код. Когда-нибудь вам может понадобится перенести программу на другую компьютерную систему, которая, возможно не будет оснащена процессором 80x86. В этом случае вы просто удалите скобки комментария и операторы asm для преобразования программы обратно в C++.

    В теле функции на ассемблере первый оператор пересылает параметр n в регистр ax. Вам не надо самим вычислять смещения (имеется в виду использование адресных выражений наподобие [bp+4]). Вы можете сослаться на любой параметр по имени переменной, и BASM восстановит соответствующий адрес. Когда регистр ax умножается на bx, результат этой операции (и функции в целом) хранится в регистрах dx:ax. Очевидно, поскольку модифицированная функция не выполняет возврат явным образом, излишняя инструкция так же уничтожается.

    Но теперь компилятор выдает предупреждение, что в функции MySquare() отсутствует оператор return. Появление этого предупреждения можно выключить с помощью директивы #pragma warn, как показано в примере. Директива #pragma warn -rvl выключает предупреждение, а директива #pragma warn +rvl включает его. После любой из этих команд вы можете написать #pragma warn .rvl для восстановления предыдущего статуса предупреждения. Можно также отключить это предупреждение в командной строке с помощью опции, подобной -wrvl. Другие комбинации предупреждений документируются в справочниках Borland.

    После оптимизации функций, подобной MySquare(), имеет смысл изучить конечный результат. Для его проверки скомпилируйте еще раз код с опцией -S и изучите получившийся ассемблерный код. Новая функция MySquare() имеет следующий вид:

   push bp           
   mov bp, sp      
   mov bx, word ptr [bp+4]
   mov ax, word ptr [bp+4]                     
   imul bx                              
   pop bp                             
   ret          

    Сравните этот код с предыдущим неоптимизированным примером. Целый параметр пересылается непосредственно в ax, и излишняя инструкция jmp отсутствует. Компилятор сам сохраняет, инициализирует и восстанавливает bp. Он также добавляет инструкцию ret.

    Как видно из этого небольшого примера, оптимизация функций требует времени и прилежания. Не стоит тратить ваше драгоценное время на оптимизацию кода, который уже работает с необходимой скоростью.

    На следующем шаге мы рассмотрим выполнение команд переходов.




Предыдущий шаг Содержание Следующий шаг