Есть ли способ, которым компилятор C / C ++ может встроить функцию C-Callback? - PullRequest
9 голосов
/ 13 марта 2011

Учитывая типичную функцию, которая принимает C-Functionpointer в качестве обратного вызова, как C-Stdlib qsort(), может ли какой-либо компилятор оптимизировать код с использованием встраивания? Я думаю, что это не может, это правильно?

int cmp(void* pa, void* pb) { /*...*/ }
int func() {
  int vec[1000];
  qsort(vec, 1000, sizeof(int), &cmp);
}

Хорошо, qsort() - это функция из внешней библиотеки, но я не думаю, что даже LTO поможет здесь, верно?

Но что, если у меня есть my_qsort(), определенный в том же модуле компиляции, тогда будет ли возможна вставка для компилятора?

int cmp(void* pa, void* pb) { /*...*/ }
void my_qsort(int* vec, int n, int sz, (void*)(void*,void*)) { /* ... */ }
int func() {
  int vec[1000];
  my_qsort(vec, 1000, sizeof(int), &cmp);
}

Имеет ли это какое-то значение? Я думаю, что использование указателя на C-функцию в качестве обратного вызова является фактором, препятствующим компиляции встраивания. Правильно?

(Я просто хочу убедиться, что понимаю, почему я должен использовать Функторы в C ++)

Ответы [ 3 ]

7 голосов
/ 13 марта 2011

Нет, это невозможно, по крайней мере с тем, как работает традиционная цепочка инструментов.Традиционный порядок операций заключается в том, что вся компиляция выполняется, а затем выполняется компоновка.

Чтобы сгенерировать встроенную функцию сравнения, компилятору сначала необходимо сгенерировать код для qsort самого встроенного (поскольку каждый экземплярqsort обычно будет использовать другую функцию сравнения).Однако в случае чего-то вроде qsort оно обычно компилируется и помещается в стандартную библиотеку еще до того, как вы начинаете думать о написании своего кода.Когда вы компилируете свой код, qsort доступен только как объектный файл.

Таким образом, чтобы иметь возможность сделать что-то подобное, вам нужно встроить возможность встраивания в компоновщик , а не компилятор.По крайней мере, в теории это возможно, но это определенно нетривиально - по крайней мере, по моей оценке, это почти наверняка сложнее, чем при работе с исходным кодом.Это также требует дублирования совсем некоторой функциональности, подобной компилятору, в компоновщике, и , вероятно, требует добавления достаточного количества дополнительной информации в объектный файл, чтобы дать компоновщику достаточно информации для работы, с которой он может даже попробоватьчтобы выполнить работу.

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

Традиционно компоновщик принципиально довольно простой вид зверя.Он начинается с объектного файла, который можно разделить на четыре основных элемента:

  1. Набор битов, которые должны быть скопированы (без изменений, если не указано иное) из объектного файла в создаваемый исполняемый файл..
  2. Список символов, содержащийся в объектном файле.
  3. Список символов, которые не включены в объектный файл.
  4. Список исправлений, для которых требуется адресбыть записанным.

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

Когда он находит объектные файлы, которые предоставляют всеСимволы, он копирует набор битовых частей каждого из них в выходной файл, и, где записи исправления говорят об этом, он записывает относительные адреса, назначенные определенным символам (например, где вы назвали printf, он вычисляетгде в исполняемом файле он скопировал биты, составляющие printf, и заполнил ваш вызов этим адресом).В относительно недавних случаях вместо того, чтобы копировать биты из библиотеки, он может вставить ссылку на общий объект / DLL в исполняемый файл и предоставить его загрузчику для фактического поиска / загрузки этого файла во время выполнения для предоставления фактического кода длясимвол.

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

Оптимизация времени соединения действительно меняет это, по крайней мере, до некоторой степени.Очевидно, что для оптимизации кода нам нужен какой-то дополнительный интеллект, который происходит в то, что традиционно считалось временем соединения.Есть (по крайней мере) два способа сделать это:

  1. встроить в компоновщик довольно много дополнительного интеллекта
  2. сохранить интеллект в компиляторе и заставить его вызывать компоновщиксделать оптимизацию.

Есть примеры обоих из них - LLVM (для одного очевидного примера) в значительной степени берет первое. Фронтальный компилятор испускает коды LLVM, а LLVM вкладывает много интеллекта / работы в его преобразование в оптимизированный исполняемый файл. gcc с GIMPLE выбирает последний путь: записи GIMPLE в основном дают компоновщику достаточно информации, чтобы он мог передать биты в ряде объектных файлов обратно компилятору, заставить компилятор оптимизировать их, а затем передать результат обратно компоновщику для фактически скопировать в исполняемый файл.

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

Теперь верно (возможно, в любом случае), что одного из них будет достаточно для осуществления оптимизации под рукой. Лично я сомневаюсь, что кто-то реализует эту оптимизацию для себя. Когда вы приступите к этому, qsort и bsearch - это почти единственные две разумно общие функции, к которым он будет / будет применяться. Для большинства практических целей это означает, что вы будете осуществлять оптимизацию исключительно ради qsort.

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

По крайней мере, теоретически это означает, что это может произойти. Однако есть еще одна проблема: совершенно независимо от оптимизации, многие компиляторы будут , а не генерировать встроенный код для рекурсивной функции. Чтобы даже попытаться это сделать, компилятор должен сначала преобразовать рекурсивную функцию в итеративную форму. Это довольно распространено в случае хвостовой рекурсии, но быстрая сортировка не является хвостовой рекурсией. Почти единственной альтернативой является реализация qsort, которая не является рекурсивной для начала. Это, конечно, возможно, но столь же необычно.

Таким образом, даже когда / если набор инструментов может поддерживать встроенную генерацию обратного вызова, это, вероятно, не будет в случае qsort (что, я признаю, является единственным случаем Я лично проверял). Однако, к лучшему или худшему, qsort - почти единственная функция такого рода, которая достаточно распространена, чтобы она также имела большое значение.

4 голосов
/ 14 марта 2011

Да, есть компиляторы, которые встроили обратные вызовы. GCC определенно может сделать это для функций, которые определены в том же модуле компиляции, и, возможно, при использовании LTO (что я не проверял, но нет ничего, что в принципе препятствует такой оптимизации).

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

1 голос
/ 13 марта 2011

Заявленный вами случай является одной из нескольких причин, по которым вы должны использовать функторы в C ++ вместо указателей на функции.

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

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

...