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

Каков самый быстрый способ обмена значениями в C?

Я хочу обменять два целых числа, и я хочу знать, какая из этих двух реализаций будет быстрее: Очевидный способ с переменной temp:

void swap(int* a, int* b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

Или версия xor, которую я уверен, что большинство людей видели:

void swap(int* a, int* b)
{
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

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

4b9b3361

Ответ 1

Метод XOR не работает, если a и b указывают на тот же адрес. Первый XOR очистит все биты в адресе памяти, на который указывают обе переменные, поэтому, как только функция вернется (* a == * b == 0), независимо от начального значения.

Дополнительная информация на странице Wiki: алгоритм обмена XOR

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

Ответ 2

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

Придерживайтесь номера 1, это самый общий и наиболее понятный swap и может быть легко шаблонизирован/обобщен.

В этом разделе википедии достаточно хорошо объясняются проблемы: http://en.wikipedia.org/wiki/XOR_swap_algorithm#Reasons_for_avoidance_in_practice

Ответ 3

На современном процессоре вы можете использовать следующее при сортировке больших массивов и не видите разницы в скорости:

void swap (int *a, int *b)
{
  for (int i = 1 ; i ; i <<= 1)
  {
    if ((*a & i) != (*b & i))
    {
      *a ^= i;
      *b ^= i;
    }
  }
}

На самом деле важной частью вашего вопроса является "почему?". часть. Теперь, вернувшись на 20 лет до 8086 дней, вышеупомянутое было бы настоящим убийцей, но на последнем Pentium это было бы подходящей скоростью для двух вы отправили.

Причина сводится к памяти и не имеет ничего общего с процессором.

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

  • Уровень кэша 1 - работает с той же скоростью, что и процессор, имеет незначительное время доступа, но мало.
  • Уровень кэша 2 - работает немного медленнее, чем L1, но больше и имеет большие служебные данные для доступа (обычно данные сначала нужно перенести на L1).
  • Уровень кеша 3 - (не всегда присутствует) Часто внешний процессор, более медленный и больший, чем L2
  • ОЗУ - основная системная память, обычно реализующая конвейер, поэтому есть латентность в запросах чтения (данные запросов ЦП, сообщение, отправленное в ОЗУ, ОЗУ получает данные, ОЗУ отправляет данные в ЦП).
  • Жесткий диск - когда не хватает ОЗУ, данные пересылаются на HD, что очень медленно, а не под управлением ЦП как таковое.

Алгоритмы сортировки ухудшают доступ к памяти, поскольку они обычно получают доступ к памяти очень неупорядоченным образом, что приводит к неэффективным издержкам извлечения данных из L2, ОЗУ или HD.

Таким образом, оптимизация метода подкачки действительно бессмысленна - если она называется только несколько раз, то любая неэффективность скрыта из-за небольшого количества вызовов, если она вызвала много, то любая неэффективность скрыта из-за количества промахов в кэше (где ЦП должен получать данные от L2 (1 из циклов), L3 (10 циклов), ОЗУ (100 циклов), HD (!)).

Что вам действительно нужно сделать, так это посмотреть на алгоритм, который вызывает метод swap. Это не тривиальное упражнение. Хотя нотация Big-O полезна, O (n) может быть значительно быстрее, чем O (log n) для малых n. (Я уверен, что есть статья CodingHorror об этом.) Кроме того, многие алгоритмы имеют вырожденные случаи, когда код делает больше, чем это необходимо (использование qsort на почти упорядоченных данных может быть медленнее, чем сортировка пузырьков с ранней проверкой). Итак, вам нужно проанализировать свой алгоритм и данные, которые он использует.

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

Что касается исходного вопроса - что быстрее? - ему нравится пытаться выяснить, Ferrari быстрее, чем Lambourgini, глядя на размер и форму зеркала крыла.

Ответ 4

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

Быстрее понять, что это самая важная часть;)

Ответ 5

@Harry: Идите в углу и подумайте о том, что вы предложили. Вернитесь, когда вы осознали ошибку своих путей.

Никогда не выполняйте функции как макросы по следующим причинам:

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

    float a=1.5f,b=4.2f;
    swap (a,b);
    

    Шаблонная функция всегда будет иметь правильный тип (и почему вы не рассматриваете предупреждения как ошибки?).

    EDIT: поскольку в C нет шаблонов, вам нужно написать отдельный своп для каждого типа или использовать доступ к хакерской памяти.

  • Это текстовая подстановка. Следующие ошибки не выполняются во время выполнения (на этот раз без предупреждений компилятора):

    int a=1,temp=3;
    swap (a,temp);
    
  • Это не функция. Таким образом, он не может использоваться в качестве аргумента для чего-то вроде qsort.

  • Компиляторы умны. Я имею в виду действительно умный. Сделано действительно умными людьми. Они могут выполнять вложение функций. Даже во время соединения (что еще более умно). Не забывайте, что вложение увеличивает размер кода. Большой код означает больше шансов пропустить кеш при извлечении инструкций, что означает более медленный код.
  • Побочные эффекты. Макросы имеют побочные эффекты! Рассмотрим:

    int &f1 ();
    int &f2 ();
    void func ()
    {
      swap (f1 (), f2 ());
    }
    

    Здесь f1 и f2 будут вызываться дважды.

    EDIT: версия C с неприятными побочными эффектами:

    int a[10], b[10], i=0, j=0;
    swap (a[i++], b[j++]);
    

Макросы: Просто скажите нет!

EDIT: Именно поэтому я предпочитаю определять имена макросов в UPPERCASE, чтобы они выделялись в коде как предупреждение для использования с осторожностью.

EDIT2: Чтобы ответить на комментарий Leahn Novash:

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

bytes = C(p) + C(f)

где C() задает количество созданных байтов, C (f) - это байты для функции, а C (p) - байты для кода "домашнего хозяйства", преамбулы и пост-амблема, который компилятор добавляет к (создание и уничтожение фрейма стека функций и т.д.). Теперь для вызова функции f требуется C (c) байт. Если функция вызывается n раз, то общий размер кода:

size = C(p) + C(f) + n.C(c)

Теперь позвольте встроить функцию. C (p) функция "домашнее хозяйство" становится нулевой, так как функция может использовать кадр стека вызывающего. C (c) также равен нулю, поскольку в настоящее время нет кода вызова вызова. Но, f реплицируется везде, где был вызов. Итак, общий размер кода теперь:

size = n.C(f)

Теперь, если C (f) меньше C (c), тогда общий размер исполняемого файла будет уменьшен. Но, если C (f) больше C (c), тогда размер кода будет увеличиваться. Если C (f) и C (c) одинаковы, вам также нужно рассмотреть C (p).

Итак, сколько байтов производит C (f) и C (c). Наилучшим образом, простейшая функция С++ была бы геттером:

void GetValue () { return m_value; }

который, вероятно, сгенерировал бы четырехбайтную инструкцию:

mov eax,[ecx + offsetof (m_value)]

который представляет собой четыре байта. Требование вызова - пять байтов. Таким образом, общая экономия размеров. Если функция более сложная, скажем, указатель ( "return m_value [index];" ) или вычисление ( "return m_value_a + m_value_b;" ), тогда код будет больше.

Ответ 6

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

#define swap(a, b)   \
do {                 \
    int temp = a;    \
    a = b;           \
    b = temp;        \
} while(0)

Ответ 7

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

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

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

Ответ 8

Никогда не понимал ненависть к макросам. При правильном использовании они могут сделать код более компактным и читаемым. Я считаю, что большинство программистов знают, что макросы должны использоваться с осторожностью, важно то, что конкретный вызов является макросом, а не вызовом функции (все кепки). Если SWAP(a++, b++); является постоянным источником проблем, возможно, программирование не для вас.

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

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

#define SWAP(type, a, b) \
    do { type t=(a);(a)=(b);(b)=t; } while (0)

void swap(size_t esize, void* a, void* b)
{
    char* x = (char*) a;
    char* y = (char*) b;
    char* z = x + esize;

    for ( ; x < z; x++, y++ )
        SWAP(char, *x, *y);
}

Ответ 9

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

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

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

Ответ 10

Я бы не стал делать это с указателями, если вам не нужно. Компилятор не может оптимизировать их очень хорошо из-за возможности сглаживания указателей (хотя, если вы можете ГАРАНТИРОВАТЬ, что указатели указывают на неперекрывающиеся местоположения, GCC по крайней мере имеет расширения для оптимизации этого).

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

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

Что-то вроде этого:

#define swap(a,b) \
  do { \
    typeof(a) temp; \
    temp = a; \
    a = b; \
    b = temp; \
  } while (0)

...    
{
  int a, b;
  swap(a, b);
  unsigned char x, y;
  swap(x, y);                 /* works with any type */
}

С другими компиляторами, или если вам требуется строгое соответствие стандарту C89/99, вам нужно будет сделать отдельный макрос для каждого типа.

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

Ответ 11

Все самые рейтинговые ответы на самом деле не являются окончательными "фактами"... это люди, которые спекулируют!

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

Вот код c, который я скомпилировал с флагами "gcc -std = c99 -S -O3 lookAtAsmOutput.c":

#include <stdio.h>
#include <stdlib.h>

void swap_traditional(int * restrict a, int * restrict b)
{
    int temp = *a;
    *a = *b;
    *b = temp;
}

void swap_xor(int * restrict a, int * restrict b)
{
    *a ^= *b;
    *b ^= *a;
    *a ^= *b;
}

int main() {
    int a = 5;
    int b = 6;
    swap_traditional(&a,&b);
    swap_xor(&a,&b);
}

Выход ASM для swap_traditional() принимает → > 11 < инструкции (не включая "оставить", "ret", "размер" ):

.globl swap_traditional
    .type   swap_traditional, @function
swap_traditional:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %edx
    movl    12(%ebp), %ecx
    pushl   %ebx
    movl    (%edx), %ebx
    movl    (%ecx), %eax
    movl    %ebx, (%ecx)
    movl    %eax, (%edx)
    popl    %ebx
    popl    %ebp
    ret
    .size   swap_traditional, .-swap_traditional
    .p2align 4,,15

Выход ASM для swap_xor() принимает → > 11 < инструкции, не включающие "оставить" и "ret":

.globl swap_xor
    .type   swap_xor, @function
swap_xor:
    pushl   %ebp
    movl    %esp, %ebp
    movl    8(%ebp), %ecx
    movl    12(%ebp), %edx
    movl    (%ecx), %eax
    xorl    (%edx), %eax
    movl    %eax, (%ecx)
    xorl    (%edx), %eax
    xorl    %eax, (%ecx)
    movl    %eax, (%edx)
    popl    %ebp
    ret
    .size   swap_xor, .-swap_xor
    .p2align 4,,15

Сводка сборки:
swap_traditional() принимает 11 инструкций
swap_xor() принимает 11 команд

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

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

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

Ответ 12

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

Ответ 13

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

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


Изменить: Метод 2 - это способ замены на месте (т.е. без использования дополнительных переменных). Чтобы закончить этот вопрос, я добавлю еще одну замену на месте с помощью +/-.

void swap(int* a, int* b)
{
    if (a != b) // important to handle a/b share the same reference
    {
        *a = *a+*b;
        *b = *a-*b;
        *a = *a-*b;
    }
}

Ответ 14

По-моему, локальные оптимизации, подобные этому, должны рассматриваться только как плотно связанные с платформой. Это имеет огромное значение, если вы компилируете это на 16-битном компиляторе uC или на gcc с x64 в качестве цели.

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

Ответ 15

х = х + у- (у = х);

float x; cout << "X:"; cin >> x;
float y; cout << "Y:" ; cin >> y;

cout << "---------------------" << endl;
cout << "X=" << x << ", Y=" << y << endl;
x=x+y-(y=x);
cout << "X=" << x << ", Y=" << y << endl;

Ответ 16

Если вы можете использовать некоторый встроенный ассемблер и выполните следующее (ассемблер psuedo):

PUSH A
A=B
POP B

Вы сэкономите много передаваемых параметров и установите код исправления и т.д.

Ответ 17

Я просто поместил оба свопа (как макросы) в рукописный quicksort, с которым я играл. Версия XOR была намного быстрее (0,1 сек), а затем с временной переменной (0,6 сек). Тем не менее, XOR испортил данные в массиве (вероятно, упоминалась одна и та же проблема Ant).

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


acopy=a;
bcopy=b;
a=bcopy;
b=acopy;

[Я просто добавляю утверждения if вокруг каждого свопа, поэтому он не будет пытаться поменять себя, а XOR теперь занимает то же самое время, что и остальные (0,6 сек)]

Ответ 18

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

Вот метод, который работает с MSVС++:

#include <stdio.h>

#define exchange(a,b)   __asm mov eax, a \
                        __asm xchg eax, b \
                        __asm mov a, eax               

int main(int arg, char** argv)
{
    int a = 1, b = 2;
    printf("%d %d --> ", a, b);
    exchange(a,b)
    printf("%d %d\r\n", a, b);
    return 0;
}

Ответ 19

Ниже код будет делать то же самое. Этот фрагмент оптимизирован для программирования, поскольку он не использует третью переменную.

  x = x ^ y;
  y = x ^ y;
  x = x ^ y;

Ответ 20

void swap(int* a, int* b)
{
    *a = (*b - *a) + (*b = *a);
}

//Мой C немного ржавый, поэтому я надеюсь, что у меня есть право:)

Ответ 21

Еще один прекрасный способ.

#define Swap( a, b ) (a)^=(b)^=(a)^=(b)

Преимущество

Нет необходимости в вызове функции и удобстве.

Минус:

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