На этом шаге мы рассмотрим оптимизацию функций.
Основная причина использования ассемблера в программах совместно с C++ - повышение производительности. Сначала с помощью ваших собственных текстов или даже лучше, с помощью текстов Turbo Profiler определите функции, выполнение которых отнимает большую часть общего времени выполнения. Оптимизация таких критических функций может значительно повысить производительность вашей программы.
Рассмотрим простой пример - функцию 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.
Как видно из этого небольшого примера, оптимизация функций требует времени и прилежания. Не стоит тратить ваше драгоценное время на оптимизацию кода, который уже работает с необходимой скоростью.
На следующем шаге мы рассмотрим выполнение команд переходов.