Удвоение числа - сдвиг, оставленный по сравнению с умножением

Между чем различия

int size = (int)((length * 200L) / 100L); // (1)

и

int size = length << 1; // (2)

(длина является интервалом в обоих случаях),

Я предполагаю, что оба фрагмента кода хотят удвоить параметр длины.

Я испытал бы желание использовать (2)... так там какие-либо преимущества для использования (1)? Я посмотрел на пограничные случаи, когда переполнение происходит, и обе версии, кажется, имеют то же поведение.

Скажите мне, что является мной пропавшие без вести.

11
задан 17 June 2010 в 07:38
поделиться

9 ответов

И вот третий вариант:

int size = length * 2; // Comment explaining what is 2 or what means this multiplication

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

РЕДАКТИРОВАТЬ Как упоминалось многими другими, просто используйте здесь любую значимую переменную вместо 2 .

27
ответ дан 3 December 2019 в 00:38
поделиться

Что более читаемо для вашего среднего программиста:

int size = length * 2;
int size = length << 1;

Если они не исходят из сильного опыта настройки битов C ++, я готов поспорить, что ваш средний программист знает немедленно , что делает первая строка (в ней даже есть число "2" для "двойного" в нем), но придется остановиться и сделать паузу для второй строки.

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

7
ответ дан 3 December 2019 в 00:38
поделиться

Что означают 200L и 100L ? Мне кажется, вы используете магические числа . Вы должны попытаться написать свою программу таким образом, чтобы она как можно лучше описывала ее намерения; сделать его максимально читаемым. Использование именованных констант для этих значений вместо магических чисел - хороший способ сделать это. Также помогает извлечение расчета в его собственном хорошо названном методе. Когда вы это сделаете, вы сразу увидите, что переписать его на x << 1 невозможно. Не потому, что результаты будут другими, а потому, что пострадает ремонтопригодность. Когда вы пишете код вроде x << 1 , следующий программист понятия не имеет, что это на самом деле означает, и он увеличит знаменитые WTF в минуту :

alt text
(источник: ] osnews.com )


Я думаю, ваш код должен выглядеть примерно так:
int size = CalculateSizeOfThing(length);

private static int CalculateSizeOfThing(int length)
{
    const long TotalArraySize = 200L;
    const long BytesPerElement = 100L;

    return (length * TotalArraySize) / BytesPerElement;
}

Конечно, названия этих const значений - это догадка: -)

6
ответ дан 3 December 2019 в 00:38
поделиться

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

В контексте checked (int.MaxValue * 200L) никогда не переполнится, потому что результат легко помещается в long. (int)((length * 200L) / 100L переполнится только при переполнении length * 2. Ни при каких обстоятельствах оператор сдвига не переполнится. Поэтому фрагменты кода ведут себя по-разному в контексте checked.

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

(Вопреки распространенному мнению x << 1 - это не то же самое, что x * 2. Они одинаковы, только если x - беззнаковое целое число.)

.
0
ответ дан 3 December 2019 в 00:38
поделиться

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

С другой стороны, выполнение (x * 200) / 100 не будет таким же, как удвоение числа, поскольку первое умножение может привести к переполнению, поэтому его нельзя безопасно оптимизировать для удвоения числа.

1
ответ дан 3 December 2019 в 00:38
поделиться

Идея о том, что << быстрее, чем умножение, основывается на том, что jit-компилятор .NET на самом деле является плохо оптимизированным компилятором C, написанным в 1970-х годах. Даже если бы это было правдой, разница была бы измерена в пикосекундах в этот момент времени, даже если бы была разница, которой, вероятно, нет.

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

Кроме того, операторы сдвига не имеют ту же семантику, что и умножение. Например, рассмотрим следующую последовательность правок:

Исходная программа Джилл:

int x = y * 2;

Правка Боба: Глупая Джилл, я сделаю это «быстрее»:

int x = y << 1;

Правка Ларри-интерна: О, у нас есть ошибка, мы разошлись по очереди, позвольте мне исправить это:

int x = y << 1 + 1;

и Ларри только что представил новую ошибку. y * 2 + 1 отличается от y << 1 + 1; последнее на самом деле y * 4.

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

Я ни разу не видел, чтобы кто-то ошибся в арифметическом приоритете, умножив его на два, написав x * 2. Люди понимают приоритет + и *. Многие люди забывают, каков приоритет переключения передач. Стоят ли пикосекунды, которые вы не спасают , какого-либо числа потенциальных ошибок? Я говорю нет.

42
ответ дан 3 December 2019 в 00:38
поделиться

Звучит как преждевременная оптимизация. Плюс простого выполнения length * 2 в том, что компилятор, безусловно, оптимизирует это, и его легче поддерживать.

2
ответ дан 3 December 2019 в 00:38
поделиться

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

Это чисто академическое упражнение; как почти все отметили, умножение легче читать (хотя зачем там часть "*200L / 100L", никто не знает - она просто служит для запутывания). Также очевидно, что замена умножения на битовый сдвиг в C# не даст существенного прироста производительности даже в узких циклах. Если вам нужна такая оптимизация, то вы используете не ту платформу и не тот язык.

Давайте посмотрим, что произойдет, если мы скомпилируем простую программу с помощью CSC (компилятора C#), входящего в состав Visual Studio 2010, с включенными оптимизациями. Вот первая программа:

static void Main(string[] args)
{
    int j = 1;

    for (int i = 0; i < 100000; ++i)
    {
        j *= 2;
    }
}

Используя ildasm для декомпиляции полученного исполняемого файла, мы получим следующий листинг CIL:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       23 (0x17)
  .maxstack  2
  .locals init ([0] int32 j,
           [1] int32 i)
  IL_0000:  ldc.i4.1
  IL_0001:  stloc.0
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.1
  IL_0004:  br.s       IL_000e
  IL_0006:  ldloc.0
  IL_0007:  ldc.i4.2
  IL_0008:  mul
  IL_0009:  stloc.0
  IL_000a:  ldloc.1
  IL_000b:  ldc.i4.1
  IL_000c:  add
  IL_000d:  stloc.1
  IL_000e:  ldloc.1
  IL_000f:  ldc.i4     0x186a0
  IL_0014:  blt.s      IL_0006
  IL_0016:  ret
} // end of method Program::Main

Вот вторая программа:

static void Main(string[] args)
{
    int j = 1;

    for (int i = 0; i < 100000; ++i)
    {
        j <<= 1;
    }
}

Декомпилировав ее, мы получим следующий листинг CIL:

.method private hidebysig static void  Main(string[] args) cil managed
{
  .entrypoint
  // Code size       23 (0x17)
  .maxstack  2
  .locals init ([0] int32 j,
           [1] int32 i)
  IL_0000:  ldc.i4.1
  IL_0001:  stloc.0
  IL_0002:  ldc.i4.0
  IL_0003:  stloc.1
  IL_0004:  br.s       IL_000e
  IL_0006:  ldloc.0
  IL_0007:  ldc.i4.2
  IL_0008:  shl
  IL_0009:  stloc.0
  IL_000a:  ldloc.1
  IL_000b:  ldc.i4.1
  IL_000c:  add
  IL_000d:  stloc.1
  IL_000e:  ldloc.1
  IL_000f:  ldc.i4     0x186a0
  IL_0014:  blt.s      IL_0006
  IL_0016:  ret
} // end of method Program::Main

Обратите внимание на разницу в строке 8. В первой версии программы используется умножение (mul), а во второй - сдвиг влево (shl).

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

5
ответ дан 3 December 2019 в 00:38
поделиться

Когда я вижу идиому:

int val = (someval * someConstant) / someOtherConstant;

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

int val = someVal * (4/5); // oops, val = 0 - probably not what was intended

или написано, чтобы избежать перехода к плавающей запятой:

int val = (int)(someVal * .8); // better than 0 - maybe we wanted to round though - who knows?

, когда я вижу эту идиому:

int val = someVal << someConstant;

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

Следует помнить, что когда вы пишете код, который имеет что-то вроде этого:

int val = expr;

, существует бесконечное количество способов создать expr, так что val всегда будет иметь одно и то же значение. Для тех, кто последует, важно учитывать, какое намерение вы выражаете в expr.

2
ответ дан 3 December 2019 в 00:38
поделиться
Другие вопросы по тегам:

Похожие вопросы: