FPO


Перевод с блога Larry Osterman’s Weblog. Оригинал здесь.

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

Причина, по которой я был удивлен заключалась в том, что я не представлял себе что кто-нибудь до сих пор использует FPO.

Что же такое FPO?

Чтобы ответить на этот вопрос, надо вспомнить предысторию.

Процессор Intel 8088 обладал чрезвычайно ограниченным набором регистров (я не учитываю сегментные регистры), это были:

 AX  [BX]  CX  DX IP
[SI] [DI] [BP] SP FLAGS

При таком ограниченном наборе регистров, у каждого регистра было свое предназначение. AX, BX, CX, и DX были регистами "Общего назначения", SI и DI — "Индексные" регистры, SP — "Stack Pointer" (указатель на стек), BP — "Frame Pointer" (указатель на фрейм стека), IP — "Instruction Pointer" (указатель инструкций) и FLAGS — регистр, доступный только для чтения, который содержал несколько битов, отражающих текущее состояние процессора (к примеру, был ли результат последней арифметической или логической операции равен 0).

Регистры BX, SI, DI и BP были особенными, потому что они могли использоваться как "индексные" регистры. Индексные регистры чрезвычайно важны для компилятора, потому что они используются для доступа к памяти через указатель. Другими словамм, если имеется структура, которая размещается в памяти по смещению 0х1234, вы можете загрузить в индексный регистр значение 0х1234 и обращаться к полям этой структуры относительно этого адреса. Например, команды:

MOV BX, [Structure]
MOV AX, [BX]+4

загрузят в регистр BX значение, содержащееся в указателе Structure, а затем поместят в регистр AX значение слова, расположенного в по адресу Structure + 4.

Еще следует отметить, что регистр SP не являлся индексным регистром. Это означало, что для доступа к переменным, расположенным на стеке, необходимо было использовать другой регистр — вот откуда появился регистр BP — он был предназначен для доступа к значениям из стека.

Когдя появился 386-й процессор, в нем регистры были расширены до 32-х бит и все 8 регистров могли использоваться в качестве индексных.

[EAX] [EBX] [ECX] [EDX] EIP
[ESI] [EDI] [EBP] [ESP] FLAGS

Это было здорово, теперь вместо 4-х регистров в качестве индекстных, компилятор мог использовать 8.

Так как индексные регистры используются для доступа к полям структур, то для компилятора они на вес золота и чем их больше, тем лучше.

Особо проницательный читатель может сообразить, что, поскольку ESP теперь также является индексным регистром, то можно больше не резервировать EBP для доступа к переменным на стеке. Другими словами, вместо:

MyFunction:
  PUSH    EBP
  MOV     EBP, ESP
  SUB      ESP, <LocalVariableStorage>
  MOV     EAX, [EBP+8]
  :
  :
  MOV     ESP, EBP
  POP      EBP
  RETD

для доступа к первому параметру на стеке (EBP+0 — это старое значение EBP, EBP+4 — это адрес возврата) можно сделать следующее:

MyFunction:
  SUB     SP, <LocalVariableStorage>
  MOV     EAX, [ESP+4+<LocalVariableStorage>]
  :
  :
  ADD     SP, <LocalVariableStorage>
  RETD

Это работает замечательно — в одночасье стало возможно использовать регистр EBP как и остальные регистры общего назначения! Парни из команды компилятора назвали эту оптимизацию "Исключение указателя на фрейм" ("Frame Pointer Omission"), а нам она известна по аббревиатуре FPO.

Но есть одна маленькая проблема, связанная с FPO.

Если посмотреть на вариант функции MyFunction без FPO, то можно увидеть, что первые две инструкции — это PUSH EBP; MOV EBP, ESP. Они обладали интересным и чрезвычайно полезным сторонним эффектом. Фактически, они создавали односвязный список, который связывал фреймы всей цепочки вызова вплоть до данной функции. При помощи EBP можно восстановить весь стек вызовов. Это было невероятно полезно для отладчиков — можно было восстанавливать стеки вызовов даже если не было отладночной информации для всех отлаживаемых модулей. К сожалению, при использовании FPO, этот список теряется — информация просто нигде не сохраняется.

Чтобы решить эту проблему, создатели компилятора поместили информацию, которая теряется при использовании FPO в PDB файл для данного двоичного модуля. Таким образом, если у вас имелась отладочная информация, вы по-прежнему могли восстановить все данные из стека.

FPO использовалась для всех модулей в Windows в версии NT 3.51, но была отключена в Vista, поскольку в ней не было необходимости — компьютеры стали настолько быстрее по сравнению с 1995 годом, что то небольшое увеличение производительности, которое давало FPO, не стоило тех проблем, которые она создавала при отладке и анализе.

Реклама
Запись опубликована в рубрике оптимизация. Добавьте в закладки постоянную ссылку.

Один комментарий на «FPO»

  1. Николай:

    Спасибо, очень толковое объяснение.

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s