Удвоение числа - сдвиг влево и умножение
В чем разница между
int size = (int)((length * 200L) / 100L); // (1)
и
int size = length << 1; // (2)
(длина - int в обоих случаях)
Я предполагаю, что оба фрагмента кода хотят удвоить параметр длины.
У меня возникнет соблазн использовать (2)... так ли какие-либо преимущества для использования (1)? Я смотрел на случаи краев, когда происходит переполнение, и обе версии имеют одинаковое поведение.
Скажите, пожалуйста, что мне не хватает.
Ответы
Ответ 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 является целым без знака.)