Подтвердить что ты не робот

Удвоение числа - сдвиг влево и умножение

В чем разница между

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

и

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

Ответ 2

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

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

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

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

int x = y * 2;

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

int x = y << 1;

Отредактируйте Larry Intern: О, у нас есть ошибка, мы отключены, позвольте мне исправить это:

int x = y << 1 + 1;

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

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

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

Ответ 3

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

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

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

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

Ответ 4

Что представляют собой 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 - дикая догадка :-)

Ответ 5

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

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

Посмотрим, что произойдет, когда мы скомпилируем простую программу с CSC (компилятором С#), включенным в 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, когда код выполняется, я не уверен, но сам компилятор С# явно не оптимизирует умножения на две степени в битах.

Ответ 6

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

Ответ 7

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

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 для тех, кто будет следовать.

Ответ 8

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

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

Ответ 9

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

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

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

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