Обмен значений двух переменных, не все способы одинаково полезны



На всяких курсах по программированию и на собеседованиях есть стандартная задача: есть 2 переменных a и b, нужно поменять местами их значения, подразумевается, что мы должны выпендриться и вместо использования буфера заюзать XOR или математику:

let a = 10;
let b = 20;
let c;

// 1 способ, с использованием буфера
c = a;
a = b;
b = c;

// 2 способ, сложение/вычитание
a = b + a;
b = a - b;
a = a - b;

// 3 способ - XOR
a = a ^ b;
b = b ^ a;
a = b ^ a;

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

Но меня интересует не это, а то, на сколько быстро/медленно эти способы работают. Давайте напишим тестик:

let a = 10;
let b = 20;
let c;

// test - 1
let start = new Date();
for(let ii=0; ii< 10000; ii++)
  for(let i=0; i<1000000; i++) {
    a = a ^ b;
    b = b ^ a;
    a = b ^ a;
  }

let stop = new Date();
let timeDiff = stop - start;
console.log('XOR', start, stop, timeDiff);

// test - 2
start = new Date();
for(let ii=0; ii< 10000; ii++)
  for(let i=0; i<1000000; i++) {
    let cc = a;
    a = b;
    b = cc;
  }

stop = new Date();
timeDiff = stop - start;
console.log('Buffer with local var', start, stop, timeDiff);

// test - 2-2
start = new Date();
for(let ii=0; ii< 10000; ii++)
  for(let i=0; i<1000000; i++) {
    c = a;
    a = b;
    b = c;
  }

stop = new Date();
timeDiff = stop - start;
console.log('Buffer with global var', start, stop, timeDiff);

// test - 3
start = new Date();
for(let ii=0; ii< 10000; ii++)
  for(let i=0; i<1000000; i++) {
    a = b + a;
    b = a - b;
    a = a - b;
  }

stop = new Date();
timeDiff = stop - start;
console.log('Math', start, stop, timeDiff);

Запускаем и смотрим:

Обмен значений двух переменных

И вот у нас уже есть пруфы, что обмен через буфер самый быстрый, даже если будем каждый раз создавать временную переменную. Следом идёт XOR, и потом математика, т.к. в теории, математические операции работаю медленнее, чем побитовые.

А теперь давайте всё это протестируем ещё и на С. Начнём с буфера:

// swaptest_buf.c
#include <stdio.h>
#include <sys/time.h>

long getMicrotime() {
  struct timeval currentTime;
  gettimeofday(¤tTime, NULL);
  return currentTime.tv_sec * (int)1e6 + currentTime.tv_usec;
}

int main() {
  int a = 1;
  int b = 9;
  int c = 0;
  
  long startTime = getMicrotime();
  
  // test 1
  for(int ii=0; ii<10000; ii++) {
    for(int i=0; i<1000000; i++) {
      c = a;
      a = b;
      b = c;
    }
  }
  
  long stopTime = getMicrotime();
  long timeDiff = stopTime - startTime;
  
  printf("a= %d, b= %d
", a, b);
  printf("startTime= %ld, stopTime= %ld, timeDiff= %ld
", startTime, stopTime, (timeDiff/1000));
  
  return 0;
}

Компилим и запускаем:

Обмен значений двух переменных

Результат как-то не впечатлил, в 4 раза медленнее, чем то же самое но на JS. Давайте ка обмажемся оптимизацией:

Обмен значений двух переменных

2.7 секунды против ~6 на JS. На самом деле это означает, что JS офигенно быстро работает, всего-то в 2 раза медленне, чем C с максимально оптимизацией.

Но давайте продолжим, C и XOR:

// swaptest_xor.c
#include <stdio.h>
#include <sys/time.h>

long getMicrotime() {
  struct timeval currentTime;
  gettimeofday(¤tTime, NULL);
  return currentTime.tv_sec * (int)1e6 + currentTime.tv_usec;
}

int main() {
  int a = 1;
  int b = 9;
  
  long startTime = getMicrotime();
  
  // test 1
  for(int ii=0; ii<10000; ii++) {
    for(int i=0; i<1000000; i++) {
      a = a ^ b;
      b = b ^ a;
      a = b ^ a;
    }
  }
  
  long stopTime = getMicrotime();
  long timeDiff = stopTime - startTime;
  
  printf("a= %d, b= %d
", a, b);
  printf("startTime= %ld, stopTime= %ld, timeDiff= %ld
", startTime, stopTime, (timeDiff/1000));
  
  return 0;
}

Обмен значений двух переменных

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

Ну и C и математика:

#include <stdio.h>
#include <sys/time.h>

long getMicrotime() {
  struct timeval currentTime;
  gettimeofday(¤tTime, NULL);
  return currentTime.tv_sec * (int)1e6 + currentTime.tv_usec;
}

int main() {
  int a = 1;
  int b = 9;
  
  long startTime = getMicrotime();
  
  // test 1
  for(int ii=0; ii<10000; ii++) {
    for(int i=0; i<1000000; i++) {
      a = b + a;
      b = a - b;
      a = a - b;
    }
  }
  
  long stopTime = getMicrotime();
  long timeDiff = stopTime - startTime;
  
  printf("a= %d, b= %d
", a, b);
  printf("startTime= %ld, stopTime= %ld, timeDiff= %ld
", startTime, stopTime, (timeDiff/1000));
  
  return 0;
}

Результат аналогичный:

Обмен значений двух переменных

Вывод

На низком уровне, для современных процессоров без разницы что выполнять: MOV/PXOR/FISUB/FIADD, разница в пределах погрешности.

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


Сообщество: AWS

Комментариев(5)


ID: #651   Создан:
Автор: Алекс

А замеряйте C++ std::swap, std::move и ещё для строк. Удивитесь

ID: #652   Создан:
Автор: Алекс
>>651

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

ID: #653   Создан:
Автор: ololoev
>>652

Да, в жс можно обменяться указателями. Но только для объектов/массивов.

let a = { name: 'a' } let b = { name: 'b' } let c = {};

console.log(a, b, c);

c = a; a = b; b = c;

console.log(a, b, c);

Тут будет обмен указателями, а не значениями. В JS если нужно обменяться именно значениями объектов, то нужно явно создавать новый объект и назначать ему свойства через Object.assign()

ID: #654   Создан:
Автор: ololoev
>>651

К сожалению C++ я хреново знаю, но да, будет время, дополню ещё и со свапом. Спасибо!

ID: #655   Создан:
Автор: Владимир

Это слишком простой пример, на котором компилятору негде развернуться. Нужно брать по сложнее задачи. JS теперь ведь тоже компилируется на лету, так что не удивительно что он весьма быстр. https://benchmarksgame-team.pages.debian.net/benchmarksgame/fastest/node-gpp.html И как можно заметить С/С++ выигрывает не только по производительности, но и по памяти.

Всего: 5 комментариев на 1 страницах

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



Сообщества