Наши партнеры

UnixForum



  • Тушенка оптом цена
  • Тушенка опт от эконом до премиум у производителя. Антикризисные цены.
  • tyshenka-optom.ru


Библиотека сайта rus-linux.net

На главную -> MyLDP -> Программирование и алгоритмические языки


Ulrich Drepper "Как писать разделяемые библиотеки"
Назад Оглавление Вперед

2.5. Улучшение генерируемого кода

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

В предыдущем тексте мы видели, что для архитектуры IA-32 функция, получающая доступ к глобальной переменной, для того, чтобы использовать операцию @GOTOFF, должна загрузить адрес таблицы GOT. Если предположить, что код на C имеет следующий вид

static int foo;
int getfoo (void)
{ return foo; }

то компилятор может, в конце концов, создать следующий код:

getfoo:
	call 1f
1: popl %ecx
	addl _GLOBAL_OFFSET_TABLE_[.-1b],%ecx
	movl foo@GOTOFF(%ecx),%eax
	ret

Фактический доступ к переменной омрачается накладныеми расодами, которые для этого требуются. Загрузка адреса GOT в регистр %ecx потребует трех инструкций. А что делать, если эта функция вызывается очень часто? Даже хуже: а что если функция getfoo будет определена как static или hidden и на нее даже нет указателя? В подобном случае в момент вызова функции адрес GOT может быть уже вычислен; по меньшей мере на платформе IA-32 адрес GOT будет одинаковым для всех функций, имеющихся в объекте DSO или в исполняемом модуле. Вычисление адреса GOT в foobar может не потребоваться. Ключевым термином в описании этого сценария является фраза "может быть". Интерфейс ABI для IA-32 не требует, чтобы при вызове загружался регистр PIC. Только если вызовы функций используют таблицу PLT, мы должны знать, что в %ebx содержится адрес таблицы GOT и в этом случае вызов может быть сделан из любого другого объекта DSO или из исполняемого модуля. То есть, в действительности мы всегда должны загружать адрес таблицы GOT.

На платформах с более эффективным набором инструкций, генерируемый код и вовсе будет достаточно хорошим.. Например, версия для x86-64 могла бы выглядеть следующим образом:

getfoo:
	movl foo(%rip),%eax
	ret

В архитектуре x86-64 предлагается режим относительной адресации данных, который исключительно полезен в ситуациях, подобных этой.

Еще один возможный вариант оптимизация связан тем, что при вызове загружается регистр PIC. На платформе IA-64 для этой цели используется регистр gp. Каждый указатель функции состоит из пары: адрес функции и значение gp. Значение gp должно быть загружено перед тем, как будет сделан вызов. В результате для нашего рабочего примера сгенерированный код может выглядеть следующим образом:

getfoo:
	addl r14=@gprel(foo),gp;;
	ld4 r8=[r14]
	br.ret.sptk.many b0

Если при вызове известно, что в вызваемой функции использует то же самое значение gp, то можно не загружать значение регистра gp. Платформа IA-32 действительно является особым случаем, но все еще очень широко распространена. Так что целесообразно искать решение.

В любом случае необходимо полностью избегать использовать регистр PIC. Мы предлагаем два возможных пути для улучшения этой ситуации. Во-первых, не использовать код, который зависит от того, где он расположен. Будет сгенерирован код, например, следующий код:

getfoo:
	movl foo,%eax
	ret

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

Второе предлагаемое решение оказывает большее влияние на весь код. Предположим, что есть расширенный пример:

static int foo;
static int bar;
int getfoo (void)
{  return foo;  }
int getboth (void)
{  return bar+getfoo();  }

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

static struct globals {
	int foo;
	int bar;
}  globals;
static int  intfoo (struct globals *g)
{  return g->foo;  }
int getfoo (void)
{  return intfoo(&globals);  }
int getboth (void)
{  return globals.bar+intfoo(&globals);  }

В коде, сгенерированном для этого примера, адрес GOT при вызове getboth не вычисляется повторно. Функция intfoo использует предоставляемый указатель и ей не нужен адрес GOT. Эта дополнительная функция была добавлена для того, чтобы сохранить семантику первого кода; теперь это просто обертка вокруг intfoo. Если можно будет написать исходный код для объекта DSO, в котором все глобальные переменные будут храниться в структуре, а во все внутренние функции будет передаваться дополнительный параметр, то на платформе IA-32 может добиться больших преимуществ.

Но следует иметь в виду, что для большинства других вариантов архитектуры код, сгенерированный для измененного примера, хуже, чем тот, который можно было бы сгенерировать для оригинала. Видно, что для архитектуры x86-64 дополнительный параметр для intfoo будет абсолютно лишним, поскольку мы можем получить доступ к глобальным переменным без использования значения GOT. На IA-64 пометка getfoo как hidden позволит избежать обращений к PLT и поэтому во время обращения к getfoo регистр gp перезагружаться не будет. Опять же, параметр будет абсолютно лишним. По этой причине всегда ставится вопрос, должна ли вообще для платформы IA-32 выполняться такая конкретная оптимизация. Поскольу до сих пор IA-32 является наиболее важной платформой, то должна быть возможность такой оптимизации.


Предыдущий раздел:   Следующий раздел:
Назад Оглавление Вперед