Удвоение числа - сдвиг влево против умножения - PullRequest
11 голосов
/ 17 июня 2010

Чем отличаются

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

и

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

(длина - int в обоих случаях)

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

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

Пожалуйста, скажите мне, что мне не хватает.

Ответы [ 9 ]

39 голосов
/ 17 июня 2010

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

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

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

Оригинальная программа Джилл:

int x = y * 2;

Редактирование Бобом: Глупая Джилл, я сделаю это "быстрее":

int x = y << 1;

Редактирование Ларри Стажером: О, у нас есть ошибка, мы на единицу, позвольте мне исправить это:

int x = y << 1 + 1;

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

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

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

26 голосов
/ 17 июня 2010

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

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

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

EDIT Как уже упоминалось многими другими, просто используйте здесь любую значимую переменную вместо 2.

7 голосов
/ 17 июня 2010

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

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

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

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

5 голосов
/ 17 июня 2010

Интересно, что в большинстве ответов утверждается, что компилятор оптимизирует умножение на степень 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 голосов
/ 17 июня 2010

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

альтернативный текст http://www.osnews.com/images/comics/wtfm.jpg

<ч /> Я думаю, ваш код должен выглядеть примерно так:

int size = CalculateSizeOfThing(length);

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

    return (length * TotalArraySize) / BytesPerElement;
}

Конечно, названия этих const значений являются диким предположением: -)

2 голосов
/ 17 июня 2010

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

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;

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

Помните, что когда вы пишете кодчто-то вроде этого:

int val = expr;

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

2 голосов
/ 17 июня 2010

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

1 голос
/ 17 июня 2010

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

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

0 голосов
/ 17 июня 2010

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

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

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

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

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