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

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

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

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

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

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 ответов


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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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


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

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


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

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

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