Понимание примера heisenbug: различная точность регистров и основная память

Я прочитал wiki-страницу о heisenbug, но не понимаю этот пример. Может ли кто-нибудь объяснить это подробно?

Одним из распространенных примеров heisenbug является ошибка, возникающая при компиляции программы с оптимизирующим компилятором, но не тогда, когда одна и та же программа скомпилирована без оптимизации (как это часто делается с целью изучения ее с помощью отладчика). Во время отладки значения, которые оптимизированная программа обычно хранит в регистрах, часто переносятся в основную память. Это может повлиять, например, на результат сравнений с плавающей точкой, поскольку значение в памяти может иметь меньший диапазон и точность, чем значение в регистре.

Ответ 1

Вот конкретный пример, недавно опубликованный:

Гейзенбаг с бесконечным циклом: выходит, если добавить распечатку

Это действительно хороший образец, потому что мы все можем воспроизвести его: http://ideone.com/rjY5kQ

Эти ошибки настолько зависят от очень точных характеристик платформы, что людям также очень трудно их воспроизвести.


В этом случае, когда "распечатка" не указана, программа выполняет сравнение с высокой точностью внутри регистров ЦП (выше, чем в double). Но чтобы распечатать значение, компилятор решает переместить результат в основную память, что приводит к неявному усечению точности. Когда он использует это усеченное значение для сравнения, это успешно.

#include <iostream>
#include <cmath>

double up = 19.0 + (61.0/125.0);
double down = -32.0 - (2.0/3.0);
double rectangle = (up - down) * 8.0;

double f(double x) {
    return (pow(x, 4.0)/500.0) - (pow(x, 2.0)/200.0) - 0.012;
}

double g(double x) {
    return -(pow(x, 3.0)/30.0) + (x/20.0) + (1.0/6.0);
}

double area_upper(double x, double step) {
    return (((up - f(x)) + (up - f(x + step))) * step) / 2.0;
}

double area_lower(double x, double step) {
    return (((g(x) - down) + (g(x + step) - down)) * step) / 2.0;
}

double area(double x, double step) {
    return area_upper(x, step) + area_lower(x, step);
}

int main() {
    double current = 0, last = 0, step = 1.0;

    do {
        last = current;
        step /= 10.0;
        current = 0;

        for(double x = 2.0; x < 10.0; x += step) current += area(x, step);

        current = rectangle - current;
        current = round(current * 1000.0) / 1000.0;
        //std::cout << current << std::endl; //<-- COMMENT BACK IN TO "FIX" BUG
    } while(current != last);

    std::cout << current << std::endl;
    return 0;
}

Редактировать: проверенная ошибка и исправить все еще экспонат: 20-Фев-17

Ответ 2

Это происходит из принципа неопределенности, который в основном гласит, что существует фундаментальный предел точности, с которой определенные пары физических свойств частицы могут быть известны одновременно. Если вы начнете наблюдать какую-то частицу слишком близко (т.е. вы точно знаете ее положение), то вы не сможете точно измерить ее импульс. (И если у вас есть точная скорость, то вы не можете сказать ее точное положение)

Итак, после этого Heisenbug - это ошибка, которая исчезает, когда вы внимательно смотрите.

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

Поэтому, если вы начнете наблюдать за ошибкой слишком внимательно, вы не будете знать, каковы ее свойства (или не сможете ее найти), что напоминает принцип неопределенности Гейзенберга и поэтому называется Гейзенбаг.

Ответ 3

Идея заключается в том, что код скомпилирован в два состояния: один - нормальный или отладочный, а другой - оптимизированный или производственный.

Так же, как важно знать, что происходит с вопросом на квантовом уровне, мы также должны знать, что происходит с нашим кодом на уровне компилятора!