будет ли когда-нибудь передача переменной интегрального типа в функции по ссылке более эффективной, чем по значению? - PullRequest
6 голосов
/ 28 сентября 2010

Я знаю, что это сказано при передаче переменной любого целочисленного типа, такого как int, double, long double и т. Д. В функцию;это должно быть сделано по значению, но мне любопытно, что с точки сборки (с точки зрения производительности или пространства) не было бы ситуации при передаче переменной целочисленного типа с размером больше, чем указатели, такие как long doubleна моей платформе, которая имеет размер 8 байт и имеет больший размер, чем указатели, имеющие размер 4 байта;по ссылке было бы эффективнее?

Ответы [ 4 ]

5 голосов
/ 28 сентября 2010

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

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

Наиболее разумная вещь, которую нужно сделать в подавляющем большинстве случаев, - это написать свой код так, чтобы он наилучшим образом сообщал о вашем намерении (передача по значению для типов «значение», если аргумент - использование терминологии C # - * 1013) * семантически параметр "out" или "reference") и беспокоиться об эффективности, только если существует явное узкое место в производительности.

4 голосов
/ 28 сентября 2010

Тест, тест, тест, разбирать, разбирать, разбирать.

Простое целое число собственного размера.

unsigned int fun_one ( unsigned int a )
{
    return((a&7)+1);
}

unsigned int fun_two ( unsigned int *a )
{
    return((*a&7)+1);
}

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

00000000 :
   0:   e52db004    push    {fp}        ; (str fp, [sp, #-4]!)
   4:   e28db000    add fp, sp, #0
   8:   e24dd00c    sub sp, sp, #12
   c:   e50b0008    str r0, [fp, #-8]
  10:   e51b3008    ldr r3, [fp, #-8]
  14:   e2033007    and r3, r3, #7
  18:   e2833001    add r3, r3, #1
  1c:   e1a00003    mov r0, r3
  20:   e28bd000    add sp, fp, #0
  24:   e49db004    pop {fp}        ; (ldr fp, [sp], #4)
  28:   e12fff1e    bx  lr

0000002c :
  2c:   e52db004    push    {fp}        ; (str fp, [sp, #-4]!)
  30:   e28db000    add fp, sp, #0
  34:   e24dd00c    sub sp, sp, #12
  38:   e50b0008    str r0, [fp, #-8]
  3c:   e51b3008    ldr r3, [fp, #-8]
  40:   e5933000    ldr r3, [r3]
  44:   e2033007    and r3, r3, #7
  48:   e2833001    add r3, r3, #1
  4c:   e1a00003    mov r0, r3
  50:   e28bd000    add sp, fp, #0
  54:   e49db004    pop {fp}        ; (ldr fp, [sp], #4)
  58:   e12fff1e    bx  lr

Оптимизация, от -O1 до -O3 дали тот же результат.И вы по-прежнему теряете инструкцию, загружающую значение.

00000000 :
   0:   e2000007    and r0, r0, #7
   4:   e2800001    add r0, r0, #1
   8:   e12fff1e    bx  lr

0000000c :
   c:   e5900000    ldr r0, [r0]
  10:   e2000007    and r0, r0, #7
  14:   e2800001    add r0, r0, #1
  18:   e12fff1e    bx  lr

И это будет продолжаться так же, как и для практически любой единицы измерения, которую вы можете передать. 64-битные целые числа, вы все еще сжигаете дополнительную инструкцию изагрузка циклов памяти из ссылки в регистры для работы.Любой массив чего-то, что вы действительно не можете сделать, передавая по значению, не так ли?Но структура, которую вы можете, и для достижения структуры, ссылки или нет, вероятно, потребует некоторой адресации.

typedef struct
{
    unsigned int a;
    unsigned int b;
    char c[4];
} ruct;

unsigned int fun_one ( ruct a )
{
    return((a.c[3]&7)+1);
}

unsigned int fun_two ( ruct *a )
{
    return((a->c[3]&7)+1);
}

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

00000000 :
   0:   e52db004    push    {fp}        ; (str fp, [sp, #-4]!)
   4:   e28db000    add fp, sp, #0
   8:   e24dd014    sub sp, sp, #20
   c:   e24b3010    sub r3, fp, #16
  10:   e8830007    stm r3, {r0, r1, r2}
  14:   e55b3005    ldrb    r3, [fp, #-5]
  18:   e2033007    and r3, r3, #7
  1c:   e2833001    add r3, r3, #1
  20:   e1a00003    mov r0, r3
  24:   e28bd000    add sp, fp, #0
  28:   e49db004    pop {fp}        ; (ldr fp, [sp], #4)
  2c:   e12fff1e    bx  lr

00000030 :
  30:   e52db004    push    {fp}        ; (str fp, [sp, #-4]!)
  34:   e28db000    add fp, sp, #0
  38:   e24dd00c    sub sp, sp, #12
  3c:   e50b0008    str r0, [fp, #-8]
  40:   e51b3008    ldr r3, [fp, #-8]
  44:   e5d3300b    ldrb    r3, [r3, #11]
  48:   e2033007    and r3, r3, #7
  4c:   e2833001    add r3, r3, #1
  50:   e1a00003    mov r0, r3
  54:   e28bd000    add sp, fp, #0
  58:   e49db004    pop {fp}        ; (ldr fp, [sp], #4)
  5c:   e12fff1e    bx  lr

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

00000000 :
   0:   e24dd010    sub sp, sp, #16
   4:   e28d3004    add r3, sp, #4
   8:   e8830007    stm r3, {r0, r1, r2}
   c:   e5dd100f    ldrb    r1, [sp, #15]
  10:   e2010007    and r0, r1, #7
  14:   e2800001    add r0, r0, #1
  18:   e28dd010    add sp, sp, #16
  1c:   e12fff1e    bx  lr

00000020 :
  20:   e5d0100b    ldrb    r1, [r0, #11]
  24:   e2010007    and r0, r1, #7
  28:   e2800001    add r0, r0, #1
  2c:   e12fff1e    bx  lr

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

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

Вам также нужно смотреть за пределы отдельных функций относительно того, сколько памяти и работы регистра имеетслучиться, чтобы подготовить вызов функции.Передача по ссылке для примера структуры будет одной загрузкой или немедленной, чтобы заполнить один регистр адресом структуры.Передача по значению структуры, в случае ARM, была бы единственной инструкцией для загрузки трех регистров со структурой, но потенциально она занимает три такта (или 6 или 2 в зависимости от шины amba / axi).Другие процессоры могут стоить вам три инструкции плюс тактовый цикл данных для каждого регистра.Таким образом, даже если бы gcc лучше справился с оптимизацией примера структуры передачи по значению, передача по ссылке могла бы просто вытеснить его за один или два такта, но это сильно зависит от того, как выглядит код в вызывающей функции.Чтобы действительно знать, что вы должны протестировать его путем точной синхронизации кода и разобрать, чтобы понять, почему он становится быстрее или медленнее, когда вы настраиваете его.

3 голосов
/ 28 сентября 2010

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

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

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

0 голосов
/ 02 октября 2010

Если вы передаете значение, которое использует только несколько вызовов функций, то может быть более эффективным для передачи по ссылке на const-T).Однако в этом случае вы раскрываете детали реализации ради преждевременной «оптимизации».

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

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

Например (я использую синтаксис указателя, чтобы сделать вещи более явными, но то же самое верно для ссылок):

long long x=0,y=1;

for (int i = 0; i < 10; i++) {
  x = f(&x);
  g(&x);

  y = f(&y);
  g(&y);
}

Довольно стандартно, но f () и g () могут раздражать:

long long f(long long * x) {
    static long long * old;
    if (old) { *old++; *x += *old; }
    return ++*x;
}

long long g(long long * x) {
    static long long * old;
    if (old == x) { abort(); }
    printf("%lld\n", *x);
}

Вы можете решить некоторые проблемы, используя long long const * (поэтому функции не могут изменить tЭто значение, но они все еще могут читать из него ...).

Обойти это можно, вставив вызов функции внутри блока и передав ссылку на копию переменной:

{
  long long tmp = x;
  x = f(&tmp);
}
...