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

UnixForum





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

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


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

2.4.4. Массив указателей на функции

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

static int a0  (int a)  { return a + 0; }
static int a1  (int a)  { return a + 1; }
static int a2  (int a)  { return a + 2; }
static int (*fps[ ])  (int) = {
      [0] = a0,
      [1] = a1,
      [2] = a2
};

int add (int a, int b) {
   return fps[b] (a);
}

Решение этой проблемы неизбежно будет отличаться от того, что мы сделали для строк, где мы объединили все строки в одну. Мы также можем сделать это для функций, но это будет выглядеть несколько иначе:

int add (int a, int b) {
   switch (b) {
   case 0:
	return a + 0;
   case 1:
	return a + 1;
   case 2:
	return a + 2;
   }
}

Предпочтительным решением должно, безусловно, быть встраивание (inline) в код, который был показан выше. Компилятор никогда не будет генерировать перемещения для реализации инструкции switch и, следовательно, для всего кода не требуется никаких перемещений.

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

Аналогичная проблема, хотя она (к сожалению) довольно редка сегодня, возникает при использовании вычисляемых переходов goto, что для gcc является расширением языка С. Вычисляемые переходы goto могут быть очень полезными при компьютерной генерации кода и в случае, когда требуется использовать максимально оптимизированный код {Примечание 11}. Предыдущий пример при использовании вычисляемых goto может выглядеть следующим образом:

int add (int a, int b) {
   static const void *labels[ ] = {
	&&a0, &&a1, &&a2
   };
   goto *labels[b];
  a0:
	return a + 0;
  a1:
	return a + 1;
  a2:
	return a + 2;
}
Примечание 11: Заинтересованные читатели могут захотеть взглянуть на реализацию vfprintf в GNU libc.

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

Приведенный выше код является, в принципе, реализацией инструкции switch. Различие лишь в том, что компилятор никогда не запоминает абсолютные адреса кода, для которых необходимы перемещения при генерации кода, не зависящего от своего положения. Вместо этого адреса вычисляются как постоянное смещение относительно адреса PIC. Затем это смещение должно быть добавлено к значению регистра PIC, для чего потребуется минимум дополнительной работы. Для оптимизации кода, приведенного выше, может быть использована аналогичная схема.

int add (int a, int b) {
   static const int offsets[ ] = {
	&&a0-&&a0, &&a1-&&a0, &&a2-&&a0
   };
   goto *(&&a0 + offsets[b]);
  a0:
	return a + 0;
  a1:
	return a + 1;
  a2:
	return a + 2;
}

Поскольку мы во время компиляции не имеем прямого доступа к регистру PIC и не можем записать правила вычисления смещений, мы должны найти другой базовый адрес. В приведенном выше примере это просто один из адресов переходов - a0. Поскольку смещения, указанные в массиве, становятся известными сразу, как только компилятор закончит для функций генерацию кода, эти смещения действительно не изменяются и помещаются в память, доступную только для чтения. Теперь у нас есть относительные адреса и перемещения не требуются. Возможно, придется выбрать тип данных, используемый для смещений., Если разница значений слишком большая (это действительно возможно только для 64-разрядной архитектуры и то лишь для чрезвычайно больших функций), тип, возможно, придется заменить на ssize t или на что-нибудь подобное. И, наоборот, если известно, что для смещений подойдут типы short или signed char, то для экономии памяти можно воспользоваться этими типами данных.


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