Указатели на функции во встроенных системах, они полезны? - PullRequest
8 голосов
/ 29 января 2011

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

Ответы [ 10 ]

21 голосов
/ 29 января 2011

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

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

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

17 голосов
/ 29 января 2011

Есть много применений.

Самым важным использованием указателей функций во встроенных системах является создание векторных таблиц. Многие архитектуры MCU используют таблицу адресов, расположенную в NVM, где каждый адрес указывает на ISR (подпрограмма обслуживания прерываний). Такая таблица векторов может быть записана в C как массив указателей на функции.

Указатели функций также полезны для функций обратного вызова. Как пример из реального мира, на днях я писал драйвер для встроенных часов реального времени. На чипе были только одни часы, но мне нужно было много таймеров. Это было решено путем сохранения счетчика для каждого программного таймера, который был увеличен из-за прерывания часов в реальном времени. Тип данных выглядел примерно так:

typedef struct
{
  uint16_t counter;
  void (*callback)(void);

} Timer_t;

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

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

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

7 голосов
/ 07 февраля 2011

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

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

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

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

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

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

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

5 голосов
/ 29 января 2011

Вы выигрываете в скорости, но теряете в читабельности и обслуживании.Вместо дерева if-then-else, если a затем fun_a (), иначе, если b, то fun_b (), еще, если c, тогда fun_c (), иначе fun_default (), и приходится делать это каждый раз, вместо этого, если тогда a fun= fun_a, иначе, если b, тогда fun = fun_b и т. д., и вы делаете это один раз, с этого момента просто вызывайте fun ().Намного быстрее.Как уже указывалось, вы не можете встроить, что является еще одним приемом скорости, но встраивание в дерево if-then-else не обязательно делает его быстрее, чем без встраивания, и в целом не так быстро, как указатель на функцию.

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

Это в основном трюк скорости, позволяющий избегать деревьев if-then-else каждый раз, когда вы хотите выполнить функцию.Если производительность не критична, если ничто иное, fun () не может быть статическим и иметь в нем дерево if-then-else.

РЕДАКТИРОВАТЬ Добавление нескольких примеров для объяснения того, о чем я говорил.

extern unsigned int fun1 ( unsigned int a, unsigned int b );

unsigned int (*funptr)(unsigned int, unsigned int);

void have_fun ( unsigned int x, unsigned int y, unsigned int z )
{
    unsigned int j;
    funptr=fun1;

    j=fun1(z,5);
    j=funptr(y,6);
}

Компиляция дает следующее:

have_fun:
    stmfd   sp!, {r3, r4, r5, lr}
    .save {r3, r4, r5, lr}
    ldr r4, .L2
    mov r5, r1
    mov r0, r2
    mov r1, #5
    ldr r2, .L2+4
    str r2, [r4, #0]
    bl  fun1
    ldr r3, [r4, #0]
    mov r0, r5
    mov r1, #6
    blx r3
    ldmfd   sp!, {r3, r4, r5, pc}

Я предполагаю, что Клиффорд говорил о том, чтопрямой вызов, если он достаточно близок (в зависимости от архитектуры), - это одна инструкция

    bl  fun1

, где указатель на функцию обойдется вам как минимум в два

    ldr r3, [r4, #0]
    blx r3

Я такжеупомянул разницу между прямой и косвенной дополнительной нагрузкой.

Прежде чем двигаться дальше, стоит упомянуть плюсы и минусы навески.В случае ARM, который используют эти примеры, соглашение о вызовах использует r0-r3 для входящих параметров функции и r0 для возврата.Таким образом, вход в have_fun () с тремя параметрами означает, что r0-r3 имеют содержимое.В ARM также предполагается, что функция может уничтожить r0-r3, поэтому have_fun () необходимо сохранить входы и затем поместить два входа в fun1 () в r0 и r1, так что происходит немного танца регистра.

    mov r5, r1
    mov r0, r2
    mov r1, #5
    ldr r2, .L2+4
    str r2, [r4, #0]
    bl  fun1

Компилятор был достаточно умен, чтобы понять, что нам никогда не требовался первый ввод для функции have_fun (), поэтому r0 был отброшен и разрешен для немедленного изменения.Кроме того, компилятор был достаточно умен, чтобы знать, что нам никогда не понадобится третий параметр, z (r2), после отправки его в fun1 () при первом вызове, поэтому ему не нужно сохранять его в старшем регистре.R1, тем не менее, второй параметр функции have_fun () должен быть сохранен, поэтому он помещается в регистр, который не будет уничтожен fun1 ().

Вы можете видеть, что то же самое происходит со второй функциейcall.

Предполагая, что fun1 () - это простая функция:

inline unsigned int fun1 ( unsigned int a, unsigned int b )
{
    return(a+b);
}

Когда вы вставляете fun1 (), вы получаете что-то вроде этого:

    stmfd   sp!, {r4, lr}
    mov r0, r1
    mov r1, #6
    add r4, r2, #5

Компилятор делаетне нужно перетасовывать нижние регистры о готовящемся звонке.Также вы могли заметить, что r4 и lr сохраняются в стеке, когда мы входим в hello_fun ().С этим соглашением о вызовах ARM функция может уничтожить r0-r3, но должна сохранить все остальные регистры, поскольку в этом случае для функции have_fun () в этом случае требуется более четырех регистров, она сохраняет содержимое r4 в стеке, чтобы она могла использоватьЭто.Аналогично, эта функция, когда я ее компилировал, вызывала другую функцию, инструкция bl / blx использует / уничтожает регистр lr (r14), поэтому для возврата функции have_fun () мы также должны сохранить lr в стеке.Упрощенный пример для fun1 () не показал этого, но другая экономия, которую вы получаете от встраивания, заключается в том, что при входе вызываемая функция не должна устанавливать кадр стека и сохранять регистры, это действительно так, как если бы вы взяли код из функциии вставил его в строку с вызывающей функцией.

Почему бы вам не вставлять все время?Ну, во-первых, он может и будет использовать больше регистров, что может привести к большему использованию стека, и стек будет медленным по сравнению с регистрами.Самое главное, что это увеличиваетразмер вашего двоичного файла, если fun1 () была функцией хорошего размера, и вы вызвали это в 20 раз в have_fun () ваш двоичный файл будет значительно больше. За современные компьютеры с гигабайтами оперативной памяти, несколько сотен или несколько десятков тысяч байты не имеют большого значения, но для встроенных с ограниченными ресурсами это может сделать или сломать тебя. На современном гигагерцовом многоядерном рабочем столе, как часто тебе нужно побрить инструкцию или пять? Иногда да но не все время для каждой функции. Так что только потому, что вы, вероятно, можете сойти с рук на рабочем столе, вероятно, не стоит.

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

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

if(outdev==OUTDEV_TAPE) data_out=data_out_tape;
else if(outdev==OUTDEV_FILE)
{
    //open the file, etc
    data_out=data_out_file;
}
...

Или, возможно, вы не знаете, работаете ли вы на процессоре с FPU или какой тип FPU у вас есть, но вы знаете, что деление с плавающей запятой Вы можете сделать это может работать намного быстрее, используя fpu:

if(fputype==FPU_FPA) fdivide=fdivide_fpa;
else if(fputype==FPU_VFP) fdivide=fdivide_vfp;
else fdivide=fdivide_soft;

И, конечно, вы можете использовать оператор case вместо if-then-else дерево, плюсы и минусы каждому, некоторые компиляторы превращают оператор дерево if-then-else в любом случае, так что это не всегда имеет значение. Дело в том, что я пытался сделать, если вы делаете это один раз:

if(fputype==FPU_FPA) fdivide=fdivide_fpa;
else if(fputype==FPU_VFP) fdivide=fdivide_vfp;
else fdivide=fdivide_soft;

И делайте это везде в программе:

a=fdivide(b,c);

По сравнению с альтернативой без указателя функции, где вы делаете это каждый где вы хотите разделить:

if(fputype==FPU_FPA) a=fdivide_fpa(b,c);
else if(fputype==FPU_VFP) a=fdivide_vfp(b,c);
else a=fdivide_soft(b,c);

Подход с использованием указателя на функцию, даже если это стоит вам дополнительного ldr на каждый звонок, намного дешевле, чем много инструкций, необходимых для дерево if-then-else. Вы платите немного вперед, чтобы настроить fdivide указатель один раз, а затем платить дополнительный LDR в каждом случае, но в целом это быстрее чем это:


unsigned int fun1 ( unsigned int a, unsigned int b );
unsigned int fun2 ( unsigned int a, unsigned int b );
unsigned int fun3 ( unsigned int a, unsigned int b );

unsigned int (*funptr)(unsigned int, unsigned int);

unsigned int have_fun ( unsigned int x, unsigned int y, unsigned int z )
{
    unsigned int j;

    switch(x)
    {
        default:
        case 1: j=fun1(y,z); break;
        case 2: j=fun2(y,z); break;
        case 3: j=fun3(y,z); break;
    }
    return(j);
}

unsigned int more_fun ( unsigned int x, unsigned int y, unsigned int z )
{
    unsigned int j;
    j=funptr(y,z);
    return(j);
}

дает нам это:

    cmp r0, #2
    beq .L3
    cmp r0, #3
    beq .L4
    mov r0, r1
    mov r1, r2
    b   fun1
.L3:
    mov r0, r1
    mov r1, r2
    b   fun2
.L4:
    mov r0, r1
    mov r1, r2
    b   fun3

вместо этого

    mov r0, r1
    ldr r3, .L7
    mov r1, r2
    blx r3

В случае по умолчанию дерево if-then-else записывает два сравнения и два перед вызовом функции напрямую. В основном иногда Дерево if-then-else будет быстрее и иногда указатель на функцию быстрее.

Еще один комментарий, который я сделал, это то, что если бы вы использовали встраивание, чтобы сделать это дерево if-then-else быстрее, вместо указателя функции, вставка всегда быстрее, верно?

unsigned int fun1 ( unsigned int a, unsigned int b )
{
    return(a+b);
}
unsigned int fun2 ( unsigned int a, unsigned int b )
{
    return(a-b);
}
unsigned int fun3 ( unsigned int a, unsigned int b )
{
    return(a&b);
}

unsigned int have_fun ( unsigned int x, unsigned int y, unsigned int z )
{
    unsigned int j;

    switch(x)
    {
        default:
        case 1: j=fun1(y,z); break;
        case 2: j=fun2(y,z); break;
        case 3: j=fun3(y,z); break;
    }
    return(j);
}

дает

have_fun:
    cmp r0, #2
    rsbeq   r0, r2, r1
    bxeq    lr
    cmp r0, #3
    addne   r0, r2, r1
    andeq   r0, r2, r1
    bx  lr

LOL, ARM помог мне в этом. Это мило. Вы можете себе представить, хотя для универсального процессора вы получите что-то вроде

    cmp r0, #2
    beq .L3
    cmp r0, #3
    beq .L4
    and r0,r1,r2
    bx lr
.L3:
    sub r0,r1,r2
    bx lr
.L4:
    add r0,r1,r2
    bx lr

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

    mov r0, r1
    ldr r1, .L7
    ldr r3,[r1]
    mov r1, r2
    blx r3

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

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

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

3 голосов
/ 29 января 2011

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

2 голосов
/ 30 января 2011

Да, они полезны.Я не уверен, к чему стремился интервьюер.В основном это не имеет значения, если система встроена или нет.Если у вас нет строго ограниченного стека.

  • Скорость Нет, самая быстрая система будет представлять собой одну функцию и использовать только глобальные переменные и разбросанные по всему.Удачи в этом.
  • Читаемость Да, это может сбить с толку некоторых людей, но в целом определенный код более читабелен с помощью указателей функций.Это также позволит вам увеличить разделение интересов между различными аспектами исходного кода.
  • Поддерживаемость Да, с указателями функций у вас будет меньше условных выражений, меньше дублирующегося кода, увеличена разделенностькод и вообще более ортогональное программное обеспечение.
1 голос
/ 07 февраля 2011

Это был вопрос с подвохом.Есть отрасли, где указатели запрещены.

1 голос
/ 29 января 2011

Еще один недостаток указателей функций (в отношении виртуальных функций, поскольку они являются ничем иным, как указателями функций на уровне ядра):

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

Эмпирическое правило: 1: не совершать виртуальные звонки в линию

1 голос
/ 29 января 2011

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

0 голосов
/ 29 января 2011

Посмотрим ...

Скорость (скажем, мы на ARM): затем (теоретически):

(нормальный размер инструкции ARM для вызова функции) <(размер указателя (-ий) инструкции установки вызова функции) </p>

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

PS: нормальный вызов функции: вызов функции, настроенный с помощью BL.

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

...