LINUX.ORG.RU

Деление целых на 2 через битовый сдвиг - архаизм?

 


1

3
#include <iostream>
using namespace std;

int main()
{
	int v = 0;
	for(int i = 0; i < 2000 * 1000 * 1000; ++i) {
		v ^= i >> 1; /* i / 2 */
	}
	cout << v << endl;
	return 0;
}

Если битовый сдвиг заменить обычным делением, то время выполнения не изменится. Обе операции насколько мне известно занимают 1 такт. Запускал на x64.
Существуют ли архитектуры (arm, mips, ...), для которых эти и другие известные трюки - полноправная оптимизация?

UPD: при делении на 3, разница между сдвигом и делением ощутимая. Вопрос: как так, ведь обе инструкции за 1 такт выполняются?



Последнее исправление: nerdogeek (всего исправлений: 2)

Ответ на: комментарий от Harald

Да, уже заметил. Но почему деление все еще выполняется медленнее чем сдвиг, там же 1 такт для обеих инструкций?

nerdogeek
() автор топика

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

prischeyadro ★★★☆☆
()

архаизм

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

buddhist ★★★★★
()
Ответ на: комментарий от nerdogeek

Обе операции насколько мне известно занимают 1 такт. Запускал на x64.

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

Harald ★★★★★
()

Как? Об косяк. Ассемблерный листинг то посмотри.

fornlr ★★★★★
()

Как ты собираешься делить на три при помощи битового сдвига? Деление на три компилятор, вероятно, должен заменить на умножение на константу. Это медленнее битового сдвига, но все еще намного быстрее деления, разумеется. Дели не на константу, тогда компилятор будет вынужден использовать настоящее деление.

trycatch ★★★
()
Ответ на: комментарий от nerdogeek

Но почему деление все еще выполняется медленнее чем сдвиг, там же 1 такт для обеих инструкций?

нет же. Зависит от камня, деление может занимать десятки тактов. Гугли 'instruction latency'

mashina ★★★★★
()

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

marvin_yorke ★★★
()

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

Вот откуда ты взял, что деление выполняется за один такт? И как ты делишь на 3 сдвигом.

anonymous
()

Обе операции насколько мне известно занимают 1 такт.
x64.

x64 - суперскалярная архитектура, забудь о тактах, и думай о том, как бы данные в кеш уместить.

Существуют ли архитектуры (arm, mips, ...), для которых эти и другие известные трюки - полноправная оптимизация?

Очевидно, что да. В том же к1810вм86 i8086 сдвиг - 4 такта, деление - десятки, точно не помню. Но если сам компилятор это умеет, то это теряет смысл.

nanoolinux ★★★★
()
Последнее исправление: nanoolinux (всего исправлений: 2)

Деление на 2 компилятор соптимизирует, деление на заранее неизвестную степень двойки - нет. Умножение - тоже. Так что сдвиг нужен.

annulen ★★★★★
()
Ответ на: комментарий от trycatch

Нет, я сравнивал деление на 2 (одинаково по скорости), потом отдельно деление на 2 сдвигом и просто деление на 3 (где-то 1 секунда в пользу сдвига).

nerdogeek
() автор топика
Ответ на: комментарий от anonymous

И как ты делишь на 3 сдвигом.

Ну если деление сдвигом на два делается с помощью «>>», то на три наверное он делит с помощью «>>>» ;)

andreyu ★★★★★
()
Ответ на: комментарий от anonymous

выставь affinity на троичное ядро твоего процессора

stevejobs ★★★★☆
()

Существуют ли архитектуры (arm, mips, ...), для которых эти и другие известные трюки - полноправная оптимизация?

Да, это полноправная оптимизация для любой архитектуры с плохим конпелятором. Не на всех ЦПУ ПЭВМ на базе ЧПУ есть деление вообще. Сюрпрайз? Так-то. А теперь вали варить борщи.

anonymous
()
Ответ на: комментарий от marvin_yorke

А вообще преждевременные оптимизации - зло.

На данный момент эта фраза уже 4.2
Потому что сейчас если не оптимизировать с самого начала (начиная еще с алгоритмов), но в итоге у тебя тетрис на i7-4990X с парой GX790 тормозить будет.

devl547 ★★★★★
()
Ответ на: комментарий от true_admin

на питоне

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

devl547 ★★★★★
()

Какой один такт - щито ты несёшь, и что несут те, кто выше меня.

Деление на штеуде занимает в овер 20тактов в лучшем случае. 64битное умножение с 64битным результатом 1такт, с 128битным - 2 такта. Сдиг, если ты сдвигаешь регистр - выполняет 0.5такта. Битовые операции 0.33. Вернее это не время выполнения, а время, через которое штеуд начнёт исполнять следующую инструкцию. Обще время выполнение у деления тактов 50, у сдвигов/битовых операций для регистров - 1такт, для операндов в памяти +~2такта.

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

osh5pntp8
()
Ответ на: комментарий от osh5pntp8

А у меня по результатам теста разница по времени выполнения между делением и сдвигом процентов 40. Хотя деление якобы 20 и более тактов, а сдвиг всего 0.5 такта.
Поэтому я ничего не понимаю что вообще происходит в данном случае...

nerdogeek
() автор топика
Ответ на: комментарий от osh5pntp8

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

osh5pntp8
()
Ответ на: комментарий от nerdogeek
int main(void) {
  uint64_t i = 1024ul*1024*1024, v = 0;
  while(--i)
    v ^= (i << 1);
  fprintf(stderr, "%lu\n", v);
}

//2427613104 cycles  
//4300874494 instructions              #    1.77  insns per cycle
//0.771506652 seconds time elapsed
int main(void) {
  uint64_t i = 1024ul*1024*1024, v = 0, d = rand();
  while(--i)
    v ^= (i/d);
  fprintf(stderr, "%lu\n", v);
}

//39895603399 cycles
//6522448852 instructions              #    0.16  insns per cycle 
//12.706586329 seconds time elapsed

Не тормазит у тебя потому, что ггц читерит и не юзает деление, а заменяет его плясками с умножением.

osh5pntp8
()
Ответ на: комментарий от nerdogeek
int main(void) {
  uint64_t i = 1024ul*1024*1024, v = 0, d = rand();
  while(--i)
    v ^= (i/666);
  fprintf(stderr, "%lu\n", v);
}

//8599377272 instructions              #    1.99  insns per cycle  
//4325258571 cycles
//1.374498496 seconds time elapsed
osh5pntp8
()
Ответ на: комментарий от osh5pntp8

Доступ к L1 - 4 такта в лучшем случае. Кроме того, на OoO-архитектурах нет вообще смысла говорить о том, сколько тактов занимает та или иная инструкция.

anonymous
()
Ответ на: комментарий от nerdogeek

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

anonymous
()
Ответ на: комментарий от anonymous

Возможно - померить полную задержку я не особо могу, а так mov l1d, r;mov r, l1d - даст задержку 2такта - исходя из этого - я то и написал.

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

osh5pntp8
()
Ответ на: комментарий от Harald

компилятор сам может заменять деление на степени двойки сдвигом

Да, я тоже так думал.

       │       mov    $0x2,%esi
       │       cltd
  0,42 │       idiv   %esi
 36,71 │       mov    %eax,%ebp
  0,84 │5df:   cmp    %r14d,%r13d
       │     ↑ jle    57c
       │       cmp    0x2c(%rsp),%r12d
       │       mov    0x10(%rsp),%r11

i-rinat ★★★★★
()
Ответ на: комментарий от true_admin

нетормозящий

pygame

Да там один pygame.init выполняется ощутимое время.

buddhist ★★★★★
()
Ответ на: комментарий от nerdogeek

Я там ещё и схалтурил немного - я не поглядел что там гцц нагинерил, а нагинерил он sse, причем как всегда криво - а ссе работают чуть медленне - поэтому числа не совсем правильные - но там порядка 5%, что не страшно.

osh5pntp8
()
Ответ на: комментарий от i-rinat

компилятор сам может заменять деление на степени двойки сдвигом

Да, я тоже так думал.

Если компилятор решил не заменять - ему виднее.

tailgunner ★★★★★
()
Ответ на: комментарий от tailgunner

Откуда вы берётесь? Не таких мест, где профитней оставлять деление вместо сдвига. Единственная причина по которой конпелятор это не заменит - это неосиляторство. Других причин нет, и быть не может, а ты, и подобные тебе не понимают, какую херню они несут.

osh5pntp8
()
Ответ на: комментарий от osh5pntp8

Единственная причина по которой конпелятор это не заменит - это неосиляторство.

Хм. Лалки анскильные, да? И характерное «нагинерил». Привет, суперхаккиллер1997, давно не виделись.

tailgunner ★★★★★
()
Последнее исправление: tailgunner (всего исправлений: 1)
Ответ на: комментарий от tailgunner

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

Внимание, вопрос: В каких случаях конпелятор должен заюзать первую операцию? Какой профит это даст, и зачем конпелятору его юзать. А так же, в чем профит не юзать 2-ю операцию?

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

osh5pntp8
()
Ответ на: комментарий от anonymous

а можно подробнее? Занять на пару часов компиляторописателя, который деление навелосипедит или кого?

anonymous
()
Ответ на: комментарий от tailgunner

Если компилятор решил не заменять - ему виднее.

Есть ещё вариант «в копиляторе баг». Очень неприятный вариант, надо сказать.

i-rinat ★★★★★
()
Ответ на: комментарий от osh5pntp8

А до тебя не доходит, что на современных x86 сдвиг будет вместо деления подставлен уже на уровне декодера инструкций?

anonymous
()
Ответ на: комментарий от osh5pntp8

В первом абзаце ты правильно пишешь, что задержку померять нельзя. Во втором пишешь, что надо мерять задержку. Шизофазия?

Кстати, задержки в два такта быть не может, пайплайн длиннее чем эти твои два такта, задержка даже у mov больше.

anonymous
()
Ответ на: комментарий от osh5pntp8

Не таких мест, где профитней оставлять деление вместо сдвига.

Их вообще-то два. Есть арифметический сдвиг вправо, а есть логический.

Absurd ★★★
()
Ответ на: комментарий от tailgunner

в замене деления на сдвиг у чисел со знаком есть какая-то засада.

И MCVC и GCC при сдвиге знаковых переменных используют SAR а не SHR. Хотя стандартом случай не оговорен.

Absurd ★★★
()
Ответ на: комментарий от anonymous
int main(void) {
  uint64_t i = 1024ul*1024*1024, v = 1 << (rand() & 0x4);
  fprintf(stderr, "%lu\n", v);
  while(--i)
    v ^= (i / v);//divq	%rbx
  fprintf(stderr, "%lu\n", v);
}
//v == 16
//6623723121 instructions              #    0.10  insns per cycle

Что-то не вижу я такого, возможно у меня старый штеуд.

osh5pntp8
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.