Является ли более эффективным выполнить проверку диапазона путем приведения к uint вместо проверки отрицательных значений?
я наткнулся на этот кусок кода в .NET список исходный код:
// Following trick can reduce the range check by one
if ((uint) index >= (uint)_size) {
ThrowHelper.ThrowArgumentOutOfRangeException();
}
по-видимому, это более эффективно (?), чем if (index < 0 || index >= _size)
мне любопытно, что стоит за этим трюком. Является ли одна инструкция ветви действительно дороже, чем два преобразования в uint
? Или есть какая-то другая оптимизация, которая сделает этот код быстрее, чем дополнительное числовое сравнение?
чтобы обратиться к слону в комната: да, это микро оптимизация, нет я не собираюсь использовать это везде в моем коде – мне просто любопытно;)
7 ответов:
С MS Partition I, раздел 12.1 (поддерживаемые типы данных):
целочисленные типы со знаком (int8, int16, int32, int64 и native int) и соответствующие им без знака целочисленные типы (неподписанные int8, INT16 в неподписанный, неподписанный типа int32, беззнаковый int64 и родной неподписанные int) отличаются только тем, как интерпретируются биты целого числа. Для тех операций, в которых целое число без знака обрабатывается иначе, чем целое число со знаком (например, в сравнения или арифметика с переполнением) есть отдельные инструкции по лечению целое число без знака (например, ВКТ.ООН и добавить.овф.ООН.)
то есть преобразование С
int
доuint
это просто вопрос бухгалтерского учета - отныне значение в стеке / в регистре теперь известно как unsigned int, а не int.таким образом, два преобразования должны быть "свободными" после того, как код JITted, а затем сравнение без знака операция может быть выполнена.
допустим, у нас есть:
public void TestIndex1(int index) { if(index < 0 || index >= _size) ThrowHelper.ThrowArgumentOutOfRangeException(); } public void TestIndex2(int index) { if((uint)index >= (uint)_size) ThrowHelper.ThrowArgumentOutOfRangeException(); }
давайте скомпилируем их и посмотрим на ILSpy:
.method public hidebysig instance void TestIndex1 ( int32 index ) cil managed { IL_0000: ldarg.1 IL_0001: ldc.i4.0 IL_0002: blt.s IL_000d IL_0004: ldarg.1 IL_0005: ldarg.0 IL_0006: ldfld int32 TempTest.TestClass::_size IL_000b: bge.s IL_0012 IL_000d: call void TempTest.ThrowHelper::ThrowArgumentOutOfRangeException() IL_0012: ret } .method public hidebysig instance void TestIndex2 ( int32 index ) cil managed { IL_0000: ldarg.1 IL_0001: ldarg.0 IL_0002: ldfld int32 TempTest.TestClass::_size IL_0007: blt.un.s IL_000e IL_0009: call void TempTest.ThrowHelper::ThrowArgumentOutOfRangeException() IL_000e: ret }
легко видеть, что второй имеет меньше кода, меньше отделения.
действительно, нет никакого броска вообще, есть выбор, использовать ли
blt.s
иbge.s
или использоватьblt.s.un
, где последний рассматривает целые числа, переданные как беззнаковые, в то время как первый рассматривает их как подписанные.(Примечание Для тех, кто не знаком с CIL, так как это a C# вопрос с CIL ответом,
bge.s
,blt.s
иblt.s.un
- это "короткие" версииbge
,blt
иblt.un
соответственно.blt
выводит два значения из стека и ветвей, если первое меньше второго при рассмотрении их как подписанных значений в то время какblt.un
стека два значения из стека и филиалы, если первое меньше второго, при рассмотрении их как беззнаковые).это совершенно микро-опт, но бывают случаи, когда микро-опт стоит делающий. Рассмотрим далее, что с остальной частью кода в теле метода это может означать разницу между чем-то, попадающим в пределы дрожания для встраивания или нет, и если они беспокоятся о том, чтобы иметь помощника для выбрасывания исключений из диапазона, они, вероятно, пытаются обеспечить встраивание, если это вообще возможно, и дополнительные 4 байта могут иметь все значение.
действительно, вполне вероятно, что эта разница в подкладке будет намного больше, чем сокращение одной ветки. Там не так много раз, когда выходите из вашем пути, чтобы обеспечить встраивание происходит, стоит, но основной метод класса такого интенсивного использования как
List<T>
наверняка был бы одним из них.
обратите внимание, что этот трюк не сработает, если ваш проект
checked
вместоunchecked
. В лучшем случае это будет медленнее (потому что каждый бросок нужно будет проверить против переполнения) (или, по крайней мере, не-быстрее), в худшем случае вы получитеOverflowException
Если вы пытаетесь пройти 1 Какindex
(вместо исключения).если вы хотите написать его "правильно" и "обязательно работать", вы должны поставить
unchecked { // test }
все вокруг теста.
предполагая, что
_size
является целым числом, частным для списка иindex
является аргументом для этой функции, валидность которой должна быть проверена.предположим далее, что
_size
всегда >= 0.тогда первоначальный тест был бы:
if(index < 0 || index > size) throw exception
The оптимизация версия
if((uint)index > (uint)_size) throw exception
имеет одно сравнение (как указано в двух int он бывший пример.) Потому что бросок просто переинтерпретирует биты и делает
>
на самом деле сравнение без знака, для него не используются дополнительные циклы процессора.почему это работает?
результаты просты / тривиальны до тех пор, пока индекс >= 0.
если индекс (uint)index превратит его в очень большом количестве:
пример: 0xFFFF -1 как int, но 65535 как uint, таким образом
(uint)-1 > (uint)x
всегда true, если
x
была положительной.
Да, это более эффективно. JIT делает тот же трюк, когда доступ к массиву проверки диапазона.
преобразование и рассуждение выглядит следующим образом:
i >= 0 && i < array.Length
становится(uint)i < (uint)array.Length
, потому чтоarray.Length <= int.MaxValue
, Так чтоarray.Length
имеет то же значение, что и(uint)array.Length
. Еслиi
тогда получается отрицательно(uint)i > int.MaxValue
и проверить не удается.
исследуя это на процессоре intel, я не нашел различий во времени выполнения, возможно, из-за нескольких целых исполнительных единиц.
но при этом на микропроцессоре реального времени 16 МГц без предсказания ветвей и целочисленных исполнительных устройств были заметные различия.
1 миллион итераций более медленного кода занял 1761 МС
int slower(char *a, long i) { if (i < 0 || i >= 10) return 0; return a[i]; }
1 миллион итераций более быстрый код занял 1635 МС
int faster(char *a, long i) { if ((unsigned int)i >= 10) return 0; return a[i]; }