Подтвердить что ты не робот

Указатель функции делает программу медленной?

Я читал о указателях функций в C. И все сказали, что моя программа будет работать медленно. Это правда?

Я сделал программу для проверки. И я получил те же результаты в обоих случаях. (измерьте время.)

Итак, плохо ли использовать указатель на функцию? Спасибо заранее.

Ответ для некоторых парней. Я сказал "бежать медленнее" на время, которое я сравнивал по петле. например:

int end = 1000;
int i = 0;

while (i < end) {
 fp = func;
 fp ();
}

Когда вы выполните это, я получаю то же самое время, если я это выполнил.

while (i < end) {
 func ();
}

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

4b9b3361

Ответ 1

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

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

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

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

// Direct call
do-it-many-times
  call 0x12345678

// Indirect call
do-it-many-times
  call dword ptr [0x67890ABC]

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

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

// Direct call

mov eax, 0x12345678

do-it-many-times
  call eax

// Indirect call

mov eax, dword ptr [0x67890ABC]

do-it-many-times
  call eax

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

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

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

Конечно, всегда есть вопрос, достаточно ли у вашего компилятора достаточно оптимизировать такие вещи...

Ответ 2

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

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

Ответ 3

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

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

Ответ 4

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

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

Ответ 5

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

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

Ответ 6

И все сказали, что сделают мой программа работает медленно. Это правда?

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

if (condition1) {
        func1();
} else if (condition2)
        func2();
} else if (condition3)
        func3();
} else {
        func4();
}

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

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

Ответ 7

Много хороших пунктов в более ранних ответах.

Однако посмотрите на функцию сравнения q qort. Поскольку функция сравнения не может быть встроена и должна следовать стандартным соглашениям на основе стека, общее время выполнения для сортировки может быть порядком (точнее, 3-10x) медленнее для целых ключей, чем в противном случае тот же код с прямым, встроенным вызовом.

Типичным вложенным сравнением будет последовательность простых CMP и, возможно, инструкций CMOV/SET. Вызов функции также приводит к накладным расходам CALL, установке кадра стека, выполнению сравнения, срыванию кадра стека и возврату результата. Обратите внимание, что операции стека могут вызывать конвейерные стойки из-за длины конвейера процессора и виртуальных регистров. Например, если значение say eax необходимо до того, как закончится выполнение последней модификации eax (что обычно занимает около 12 тактов на новейших процессорах). Если CPU не сможет выполнить другие инструкции, не дожидаясь этого, произойдет срыв конвейера.