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

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

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;
}

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

XOR медленнее. Используйте Godbolt, чтобы проверить количество инструкций ассемблера для обеих функций. Примечание, что если вы будете использовать метод XOR для значений вместо значений, хранящихся под указателем, скорость будет такой же (по крайней мере, для компилятора GCC)

fider 21.09.2017 02:04
godbolt.org/z/nqVb9q
teknoraver 13.11.2019 04:40
Похоже, что первый использует дополнительный регистр More than a bit late here, but why would anyone think that? The belief that bit-twiddling is faster than using a temporary variable ignores the reality of how most computers work, with separate CPUs and memory. A swap using a temporary variable is likely implemented as "load A into register 1, load B into register 2, save register 1 to B, save register 2 to A". "Load both variables into registers, twiddle a bits around, then do two save operations" is slower. Вы должны загрузить оба и сохранить оба, бит-тидлинг по пути лишний.
Andrew Henle 18.03.2020 16:58
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
65
3
54 453
21
Перейти к ответу Данный вопрос помечен как решенный

Ответы 21

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

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

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

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

Dan Lenski 01.10.2008 07:19

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

user25148 05.03.2009 21:35

У меня создалось впечатление, что swap, по крайней мере, на x86, действительно просто вызывает три последовательных xor.

warren 09.09.2009 11:35

@warren: xchg% eax,% eax буквально представляет собой стандартный однобайтовый код инструкции NOP. Он не обнуляет% eax, поэтому не использует xor.

Peter Cordes 05.08.2014 19:36

@PeterCordes - зачем обнулять% eax?

warren 05.08.2014 23:19

@warren - Я хочу сказать, что использование xchg с одним и тем же местоположением в обоих аргументах не обнуляет это местоположение, поэтому xchg не использует xor внутри себя. Если вы имели в виду не asm-инструкцию, то какой swap вы имели в виду? C++ std::swap?

Peter Cordes 06.08.2014 09:32

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

Быстрее конечно, что самое главное;)

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

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

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

Ответ принят как подходящий

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

Более подробная информация на странице Wiki: Алгоритм замены XOR

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

Предотвратить алиасинг довольно просто, добавив условие * a! = * B.

user9282 20.09.2008 10:48

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

Matt Curtis 22.01.2009 06:29

@mamama тоже должно быть a! = b, а не * a! = * b; ошибка - если адрес такой же, а не значение.

configurator 04.02.2009 18:17

Это может быть либо - вам не нужно менять местами, если значения уже совпадают. Но проверка (a! = B) имеет больше смысла.

Greg Rogers 05.03.2009 21:12

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

vonbrand 02.02.2013 01:05

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

PUSH A
A=B
POP B

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

будьте осторожны: vC++ не разрешает встроенный asm в 64-битном режиме. надеюсь, что это актуально или понятно так :)

Joao Vilaca 12.01.2009 04:30

Это меняет местами содержимое двух регистров, а не мест, на которые они указывают. Встроенный ASM также делает компиляторы гораздо менее способными к оптимизации, поэтому это не стоит того, если вы не делаете это для инструкций SSE или ваш встроенный asm не включает внутренний цикл.

Peter Cordes 05.08.2014 19:27

В сборке также есть команда xchg, которая меняет местами два значения.

Palle 04.11.2015 02:01

Что за придирки для ... 1) Псевдо-код, я не буквально проталкиваю регистр «А», бла-бла. 2) Опять же, псевдо-код, не ссылающийся на какой-либо конкретный ассемблер (xchg). 3) Многие люди не используют 64-битный vC++ (aaargh).

Tim Ring 11.10.2016 12:47

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

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

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

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

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

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


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

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

Мне нравится эта оценка! «Это было быстрее, но повредило данные». Классический.

unwind 10.03.2009 15:50

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

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, RAM или HD.

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

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

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

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

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

Ken White 06.03.2009 02:37

@Ken White: Я согласен, и более того, если профилирование показывает, что большая часть времени тратится на подкачку, это, скорее всего, связано с тем, что вы меняете слишком много раз (кого-то сортируете пузырями?), А не меняете медленно.

David Rodríguez - dribeas 22.07.2010 02:36

В дополнение к тому, что жесткий диск намного медленнее, чем ОЗУ, замена также означает, что вам нужно выполнить какой-то совершенно другой фрагмент кода, который, вероятно, находится в ОЗУ, но почти наверняка не будет в кеше L1 и, вероятно, не в L2 (если только у вас серьезно не хватает RAM и вы меняете постоянно). Поэтому, прежде чем что-то полезное будет сделано, ЦП должен получить ту часть кода диспетчера памяти, которая фактически выполняет подкачку.

user 14.06.2013 12:26

Хотя ваша основная точка зрения верна, показанный вами код намного медленнее, чем две версии, указанные в вопросе: Afaik, вы получаете четыре int в одной строке кеша, это означает, что в среднем вы получаете задержку менее 30 циклов для при загрузке данных (не считая предварительной выборки) у вас есть условные переходы в вашем цикле (современные архитектуры ненавидят их неверное предсказание), поэтому вы получаете гораздо, намного больше, чем цикл для каждой итерации цикла. Я готов поспорить, ваш своп займет не менее 100-200 циклов, возможно, больше, но это сильно зависит от чисел, которые вы меняете местами (сколько ошибочных прогнозов сделано).

cmaster - reinstate monica 10.10.2013 16:10

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

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

+1. Это способ сделать это в C, когда вам нужна скорость. Макрос даже можно сделать гибким по типу, если вы используете расширение typeof (), предлагаемое GNU C.

Dan Lenski 01.10.2008 07:17

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

Johannes Schaub - litb 05.03.2009 20:10

Эээ ... Зачем вам использовать компилятор, который не может делать собственное встраивание? Используйте функции, когда можете, и макросы, когда необходимо. Функции типобезопасны, их легче понять. Будет ли этот макрос делать правильные вещи с "swap (a ++, b ++)"?

John Nilsson 06.03.2009 02:41

Если вы используете достойный компилятор, вы можете использовать typeof(a) или decltype(a), чтобы сделать его более универсальным. Также, вообще говоря, вы должны добавить круглые скобки, чтобы избежать проблем с приоритетом (например, #define foo(a, b) bar(a, b, (a) + (b))).

Joey Adams 18.12.2010 09:30

Это ужасное решение. Он тихо выйдет из строя для поплавков. Здесь также отсутствуют круглые скобки.

Petter 06.09.2012 00:07

Почему цикл do / while?

jjxtra 05.08.2013 18:33

@PsychoDad Возможно, чтобы ограничить область действия временной переменной. Хотя я бы сказал, что (1) вы могли бы с таким же успехом использовать простой блок, хотя этот разрыв мог, если вы поместите использование макроса swap(a,b) в неожиданное место, так же, как и этот код, и (2) этот метод имеет более серьезные проблемы чем изоляция имен, как указывалось в предыдущих комментариях.

user 10.10.2013 15:02

@Michael Обертка do {} while (0) заставляет макрос с несколькими операторами работать в любом месте, где мог бы быть вызов функции, и ожидает следующей точки с запятой. например if (foo) swap(a,b); else swap (a, foo);

Peter Cordes 06.08.2014 15:41

@John: копирование моего комментария из другого ответа: typeof часто позволяет вам писать макросы, которые избегают оценки своих аргументов более одного раза. #define SWAP_BY_REF(a,b) do{ typeof(a) _a = (a); typeof(b) _b = (b); typeof(*_a) tmp=*_a; *_a=*_b; *_b=tmp;}while(0). Или вы можете использовать _a=&a, чтобы использовать его для значений. Будем надеяться, что компиляторы все же смогут оптимизировать хранение регистров в памяти, чтобы у них был адрес для замены двух локальных переменных, которые уже были в регистрах. Заголовочные файлы GNU libc часто используют уловку _a=(a) в макросах; вот где я впервые увидел это.

Peter Cordes 06.08.2014 15:44

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

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

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

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

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

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

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

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

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

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

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

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

Макросы: Просто сказать нет!

Обновлено: вот почему я предпочитаю определять имена макросов в ЗАПИСИ, чтобы они выделялись в коде как предупреждение, которое следует использовать с осторожностью.

РЕДАКТИРОВАТЬ2: Чтобы ответить на комментарий Лиана Новаша:

Предположим, у нас есть не встроенная функция 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). Ну, простейшей функцией C++ будет геттер:

void GetValue () { return m_value; }

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

mov eax,[ecx + offsetof (m_value)]

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

Ваш код побочного эффекта - C++, а не C (в C нет ссылок). У программистов на C нет шаблонных функций ... которые могут иметь некоторую безопасность типов, но являются настоящим кошмаром для синтаксического анализа и реализации иным образом. C++! = C. У них разные типы и степени абстракции и соглашения.

Dan Lenski 01.10.2008 07:16

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

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

мне нравится твой ответ. это было первое, что пришло мне в голову. вы можете захотеть добавить использование "register" для кода c99, который также сообщает компилятору, что они не являются псевдонимами (может использоваться, если программист знает, что аргументы - это не те же объекты)

Johannes Schaub - litb 05.03.2009 20:12

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

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

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

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

Вот код c, который я скомпилировал с флагами "gcc -std = c99 -S -O3 lookingAtAsmOutput.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

.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

.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, которому нужна скорость.

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

Adam Rosenfield 05.03.2009 21:55

Я включил оптимизацию с помощью «-o3» и даже использовал ключевое слово «restrict», чтобы обеспечить оптимизацию компилятора. Что еще мне не хватает? --- Допустим, количество циклов, которое я подсчитал, не является абсолютным. Но я по крайней мере думаю, что это будет относительный подсчет? Итак, трад. метод по-прежнему выигрывает?

Trevor Boyd Smith 05.03.2009 22:55

-o3 говорит «назовите выходной файл 3». Вам нужно -O3 (с большой буквы).

Adam Rosenfield 07.03.2009 04:03

На конвейерном суперскалярном (то есть временном) ЦП вы не можете просто подсчитать количество инструкций в ассемблерном коде и назвать это «циклами».

bendin 10.03.2009 15:53

Да, я ошибаюсь, говоря, что «каждая строка представляет собой цикл», но цель моего сообщения состояла в том, чтобы определить, какой код «быстрее относительно другого», и сравнение количества строк в каждом листинге asm все равно покажет, какой код быстрее (даже хотя каждая строка на самом деле не "сколько циклов").

Trevor Boyd Smith 11.03.2009 04:07

Как люди уже отмечали в других ответах, посмотрите на количество обращений к памяти в обоих кодах. Два move (%edx) и два move (%ecx) в первом, но по три каждого во втором. Они не являются дорогостоящими (в кеш-памяти 1-го уровня), но не могут быть удалены в этом случае (правило сглаживания указателей).

Patrick Schlüter 04.03.2010 20:33

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

cmaster - reinstate monica 10.10.2013 16:28

Версия перемещения имеет дополнительную пару инструкций push / pop, которую вы учитываете. Если у вас есть не встроенный вызов функции подкачки, ваша производительность уже получает больший удар, чем move по сравнению с xor. (особенно для ABI с параметрами в стеке, например x86). x86-64, я думаю, предоставляет некоторые регистры, которые разрешено использовать функциям без сохранения. Но в любом случае после встраивания своп, вероятно, произойдет внутри цикла, в то время как push / pop происходит только один раз. (А на x86-64 давление в регистре не так уж плохо, чтобы после этого, вероятно, нужно было перезагрузить что-то еще.)

Peter Cordes 05.08.2014 19:16

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

Peter Cordes 05.08.2014 19:20

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

alecov 04.02.2017 03:09

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

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

#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;
}

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

Peter Cordes 05.08.2014 19:30

верно. я не знал об этом ... спасибо, что просветили меня :)

jheriko 01.07.2015 18:29

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

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

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

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

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

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

Недостаток:

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

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

По общему признанию, уловка xor удобна в первые 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);
}

Усечено? Возможно, Шугар Ричард был бы более уместен в сумерках великого сыщика.

SugarD 25.02.2013 20:30

Чем это лучше функции?

Sulthan 05.12.2013 20:24

typeof часто позволяет вам писать макросы, которые избегают многократной оценки своих аргументов. #define SWAP_BY_REF(a,b) do{ typeof(a) _a = (a); typeof(b) _b = (b); typeof(*_a) tmp=*_a; *_a=*_b; *_b=tmp;}while(0). Или вы можете сделать _a = & a, чтобы вы могли использовать его для значений, а не для указателей. Будем надеяться, что компиляторы все же смогут оптимизировать хранение регистров в памяти, чтобы у них был адрес для замены двух локальных переменных, которые уже были в регистрах. Заголовочные файлы GNU libc часто используют уловку typeof(a) _a=(a) в макросах; вот где я впервые увидел это.

Peter Cordes 06.08.2014 15:36

@PeterCordes typeof - это расширение, специфичное для GCC.

yyny 12.11.2017 22:24

Для современных архитектур ЦП метод 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;
    }
}

на самом деле, для замены +/- на месте на самом деле не критично сначала обеспечить a!=b. Предположим, мы добавляем строку перед объявлением константной переменной const int C = *a, так что C == *a и C == *b верны. Тогда: *a = *a + *b -> *a равно C+C; *b = *a - *b -> *b равно C+C-C, т.е. просто C; *a = *a - *b -> *a равно C+C-C, т.е. просто C; => *a == C, *b == C -> ОК

CrepeGoat 11.07.2016 16:37

@Shillard Это может быть не критично, но полезно пропустить ненужные свопы. :П

herohuyongtao 12.07.2016 06:02

Я не рекомендую добавлять в код логическую ветвь, если она не добавляет функциональности. (Конечно, это оправдано, если вы проверили скорость, чтобы это было выгодно для вашей конкретной ситуации, то есть в 70 +% случаев a==b или что-то в этом роде ... но поскольку это общий ответ, и, следовательно, нет конкретной ситуации, логическую ветвь лучше не учитывать.) Также комментарий «важно для обработки a / b использовать одну и ту же ссылку» в вашем коде неточен.

CrepeGoat 29.09.2016 05:11

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

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

Добро пожаловать в SO! Пожалуйста, поймите, что этот вопрос датируется 2008 годом (7 лет назад), и что ваш ответ уже является частью этого вопроса. OP на самом деле спрашивал о скорости, а не о памяти.

ghybs 09.11.2015 07:50

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

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;

Это игнорирует возможность целочисленного переполнения и результирующего неопределенного поведения.

Andrew Henle 18.03.2020 17:08

Другие вопросы по теме