Я написал простой benchmark, чтобы выяснить, можно ли исключить проверку границ, когда массив вычисляется поразрядным образом. Это в основном то, что делают почти все хэш-таблицы: они вычисляют
h & (table.length - 1)
как индекс в table
, где h
- это hashCode
или производное значение. Результаты показывают, что проверка границ не устраняется.
Идея моего теста довольно проста: выведите два значения i
и j
, где оба гарантированно будут действительными индексами массива.
-
i
- это счетчик циклов. Когда он используется как индекс массива, проверка границ удаляется. -
j
вычисляется какx & (table.length - 1)
, гдеx
- некоторое изменение значения на каждой итерации. Когда он используется как индекс массива, проверка границ не устраняется.
Соответствующая часть выглядит следующим образом:
for (int i=0; i<=table.length-1; ++i) {
x += result;
final int j = x & (table.length-1);
result ^= i + table[j];
}
В другом эксперименте используется
result ^= table[i] + j;
вместо этого. Разница в сроках составляет 15% (довольно последовательно в разных вариантах, которые я пробовал). Мои вопросы:
- Существуют ли другие возможные причины для этого, кроме связанного исключения проверки?
- Есть ли какая-то сложная причина, по которой я не вижу, почему нет ограничения на проверку для
j
?
Резюме ответов
Ответ МаркоТополника показывает, что все это сложнее, и устранение проверок границ не гарантируется как победа, особенно на его компьютере "нормальный" код медленнее, чем "замаскированный". Я предполагаю, что это связано с тем, что это позволяет сделать некоторую дополнительную оптимизацию, которая в этом случае оказывается на самом деле вредной (учитывая сложность текущих процессоров, компилятор даже не знает наверняка).
leventov answer ясно показывает, что проверка границ массива выполняется в "masked" и что ее устранение делает код столь же быстрым, как "normal".
Donal Fellows указывает на то, что маскирование не работает для таблицы нулевой длины, так как x & (0-1)
равно x
. Таким образом, лучшее, что может сделать компилятор, это заменить проверку привязки проверкой нулевой длины. Но это ИМХО все еще стоит того, так как проверка нулевой длины может быть легко удалена из цикла.
Предлагаемая оптимизация
Из-за эквивалентности a[x & (a.length - 1)]
выбрасывается тогда и только тогда, когда a.length == 0
, компилятор может сделать следующее:
- Для каждого доступа к массиву проверьте, был ли вычисляемый индекс побитовым и.
- Если да, проверьте, был ли один из операндов рассчитан как длина минус единица.
- Если это так, замените проверку границ проверкой нулевой длины.
- Пусть существующие оптимизации позаботятся об этом.
Такая оптимизация должна быть довольно простой и дешевой, поскольку она смотрит только на родительские узлы в графе SSA. В отличие от многих сложных оптимизаций, он никогда не может быть вредным, поскольку он заменяет только одну проверку немного более простой; поэтому нет проблем, даже если он не может быть удален из цикла.
Я отправлю это в списки рассылки hotspot-dev.