Как правильно написать функцию, которая добавляет два целых числа в C - PullRequest
2 голосов
/ 18 марта 2019

Я новичок в C и чувствую, что наиболее оптимизировано и как правильно обращаться с указателями, значениями, ссылками и т. Д.

Я начал с создания простого целого числа add function.

int
add(int a, int b) {
  return a + b;
}

void
main() {
  // these work
  int sum = add(1, 1);
  int a = 1;
  int b = 1;
  int c = add(a, b);

  // this doesn't
  int d = add(&a, &b);
  int e = add(*a, *b);
}

Насколько я понимаю, выполнение add(a, b) скопирует значения в функцию, что означает, что это медленнее с точки зрения производительности, чем передача указателей.Поэтому я пытаюсь создать две функции добавления, переименовав эту функцию в add_values.

int
add_pointers(int *a, int *b) {
  return (*a) + (*b);
}

int
add_values(int a, int b) {
  return a + b;
}

void
main() {
  // these work
  int sum = add_values(1, 1);
  int a = 1;
  int b = 1;
  int c = add_values(a, b);

  // this works now
  int d = add_pointers(&a, &b);

  // not sure
  int e = add(*a, *b);
}

Мне интересно несколько вещей:

  1. Если есть способ получитьповедение обеих этих функций в одной функции.Или, если нет (или если производительность не оптимальна в этом случае), как обычно обрабатывать оба случая, если это просто две отдельные функции с соглашением об именах или что-то подобное, и т. Д.
  2. Какая из них является наиболее оптимизированной формой,Насколько я понимаю, это add_pointers, потому что ничего не копируется.Но тогда вы не можете сделать простой add(1, 1), так что это неинтересно с точки зрения API.

Ответы [ 3 ]

1 голос
/ 18 марта 2019

, что означает, что производительность медленнее, чем передача указателей

Вот где ты ошибся. На типичном 32-разрядном компьютере int являются 32-разрядными, а указатели - 32-разрядными. Таким образом, фактический объем передачи данных одинаков для обеих версий. Тем не менее, использование указателей может сводиться к косвенному доступу к машинному коду, поэтому в некоторых случаях он может привести к менее неэффективному коду. В общем случае int add(int a, int b), вероятно, является наиболее эффективным.

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

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

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

1 голос
/ 27 марта 2019

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

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

потому что компилятор, вероятно, вообще избегает вызова / возврата подпрограммы и будет использовать лучшее расширение, доступное в каждом случае использования (в большинстве случаев это будет встроено как одна add r3, r8 инструкция). Это может занять во многих современных многоядерных и конвейерных процессорах менее одного такта.

Если у вас нет доступа к такому компилятору, то, вероятно, лучший способ имитировать этот сценарий:

#define add(a,b) ((a) + (b))

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

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

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

EDIT

Я попробовал следующий пример и скомпилировал его в архитектуре arm7 (raspberry pi B +, компилятор freebsd, clang), и результат был далеко не хорошим:)

inline.c

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

int main()
{
    int a = 3, b = 2, c = sum(a, b);
}

в результате:

    /* ... */
    .file   "inline.c"
    .globl  main                    @ -- Begin function main
    .p2align        2
    .type   main,%function
    .code   32                      @ @main
main:
    .fnstart
@ %bb.0:
    mov     r0, #0
    bx      lr
.Lfunc_end0:
    .size   main, .Lfunc_end0-main

Как видите, единственный код для main состоял в сохранении 0 возвращаемого значения в r0 (код выхода);)

На всякий случай я скомпилировал add как функцию внешней библиотеки:

int add(int a, int b);

int main()
{
    int a = 3, b = 2, c = sum(a, b);
}

приведет к:

    .file   "inline.c"
    .globl  main                    @ -- Begin function main
    .p2align        2
    .type   main,%function
    .code   32                      @ @main
main:
    .fnstart
@ %bb.0:
    .save   {r11, lr}
    push    {r11, lr}
    .setfp  r11, sp
    mov     r11, sp
    mov     r0, #3
    mov     r1, #2
    bl      sum                 <--- the call to the function.
    mov     r0, #0
    pop     {r11, pc}
.Lfunc_end0:
    .size   main, .Lfunc_end0-main
    .cantunwind
    .fnend

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

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

1 голос
/ 18 марта 2019

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...