Почему подсчет до 2 ^ 24 выполняется быстро, но отсчет до 2 ^ 25 занимает гораздо больше времени?

Я возился вокруг создания бесконечных циклов, чтобы проверить другой код/​​мое понимание, и наткнулся на это странное поведение. В приведенной ниже программе подсчет от 0 до 2 ^ 24 занимает 100 мс на моей машине, но учет до 2 ^ 25 занимает на порядок больше времени (на момент написания, он все еще выполняется).

Почему это так?

Это было в Java 1.8.0_101, на 64-битной копии Windows 10.

TestClass.java

public class TestClass {
    public static void main(String[] args) {
        addFloats((float) Math.pow(2.0, 24.0));
        addFloats((float) Math.pow(2.0, 25.0));
    }

    private static void addFloats(float number) {
        float f = 0.0f;
        long startTime = System.currentTimeMillis();

        while(true) {
            f += 1.0f;
            if (f >= number) {
                System.out.println(f);
                System.out.println(number + " took " + (System.currentTimeMillis() - startTime) + " msecs");
                break;
            }
        }
    }
}

Ответ 1

Это связано с тем, что float имеет минимальную точность, которая может быть представлена, что уменьшилось по мере увеличения значения float. Где-то между 2 ^ 24 и 2 ^ 25 добавление одного уже недостаточно, чтобы изменить значение на следующее наибольшее представимое число. В этот момент, каждый раз через цикл, f просто сохраняет одно и то же значение, так как f += 1.0f больше не изменяет его.

Если вы измените свой цикл на это:

while(true) {
    float newF = f + 1.0f;
    if(newF == f) System.out.println(newF);
    f += 1.0f;
    if (f >= number) {
        System.out.println(f);
        System.out.println(number + " took " + (System.currentTimeMillis() - startTime) + " msecs");
        break;
    }
}

Вы можете видеть это. Кажется, что он перестает расти, как только f достигает 2 ^ 24.

Вывод вышеуказанного кода будет бесконечным числом "1.6777216E7", если вы запустите его с помощью 2 ^ 25.

Вы можете проверить это значение с помощью функции Math.nextAfter, которая сообщает вам следующее отображаемое значение. Если вы попытаетесь запустить этот код:

float value = (float)Math.pow(2.0, 24.0);
System.out.println(Math.nextAfter(value, Float.MAX_VALUE) - value);

вы можете видеть, что следующее представимое значение после 2 ^ 24 равно 2 ^ 24 + 2.

Для отличной разбивки того, почему это происходит, и почему это начинает иметь значение там, где это происходит, см. этот ответ