Оптимизация в мире 64-битных ошибок

В предыдущей записи блога я обещал рассказать, почему сложно демонстрировать 64-битные ошибки на простых примерах. Разговор касался operator[] и я говорил, что в простых случаях может работать даже явно некорректный код. Сейчас я приведу такой пример:


class MyArray

{

public:

  char *m_p;

  size_t m_n;

  MyArray(const size_t n)

  {

    m_n = n;

    m_p = new char[n];

  }

  ~MyArray() { delete [] m_p; }


  char &operator[](int index)

    { return m_p[index]; }

  char &operator()(ptrdiff_t index)

    { return m_p[index]; }


  ptrdiff_t CalcSum()

  {

    ptrdiff_t sum = 0;

    for (size_t i = 0; i != m_n; ++i)

      sum += m_p[i];

    return sum;

  }

};


void Test()

{

  ptrdiff_t a = 2560;

  ptrdiff_t b = 1024;

  ptrdiff_t c = 1024;


  MyArray array(a * b * c);


  for (ptrdiff_t i = 0; i != a * b * c; ++i)

    array(i) = 1;


  ptrdiff_t sum1 = array.CalcSum();


  for (int i = 0; i != a * b * c; ++i)

    array[i] = 2;


  ptrdiff_t sum2 = array.CalcSum();


  if (sum1 != sum2 / 2)

    MessageBox(NULL, _T("Normal error"),

        _T("Test"), MB_OK);

  else

    MessageBox(NULL, _T("Fantastic"),

        _T("Test"), MB_OK);

}


Вот кратко, что делает этот код:



  1. Создает массив размером 2.5 гигабайта (более INT_MAX элементов).

  2. Заполняет массив единицами, используя корректный operator() с параметром ptrdiff_t.

  3. Считает сумму всех элементов и помещает ее в переменную sum1.

  4. Заполняет массив двойками, используя некорректный operator[] с параметром int. Теоретически int не позволяет нам обратиться к элементам с номерами более чем INT_MAX. Есть и вторая ошибка, допущенная в цикле "for (int i = 0; i != a * b * c; ++i)". В качестве индекса мы также используем int. Эта двойная ошибка сделана, чтобы компилятор не выдавал предупреждений о преобразовании 64-битного значения в 32-битное. Фактически должно произойти переполнение и обращение к элементу с отрицательным номером, что повлечет аварийное завершение программы. Это кстати и происходит в debug-версии.

  5. Считает сумму всех элементов и помещает ее в переменную sum2.

  6. Если (sum1 == sum2 / 2), то произошло невозможное, и выводится сообщение "Fantastic".


Несмотря на две ошибки в приведенном коде он успешно работает в 64-битном release-варианте и выводит сообщение "Fantastic"!


Давайте теперь разберемся почему. Дело в том, что компилятор угадал наше желание заполнить массив значением 1 и 2. И в обоих случаях оптимизировал наш код с использованием вызова функции memset:



Вывод первый - компилятор в плане оптимизации кода молодец. Вывод второй - не следует терять бдительность.


Данная ошибка может быть легко обнаружена в debug-версии, где оптимизации нет, и код, записывающий двойки в массив, приводит к аварийному завершению. Опасность в том, что данный код некорректно ведет себя только при работе с большими массивами. Скорее всего, обработка более двух миллиардов элементов будет отсутствовать в юнит-тестах, запускаемых для отладочной версией. А release-версия может долго хранить в тайне эту ошибку. Это ошибка может совершенно неожиданно проявиться при малейшем изменении кода. Посмотрите, что произойдет, если ввести одну дополнительную переменную n:


void Test()

{

  ptrdiff_t a = 2560;

  ptrdiff_t b = 1024;

  ptrdiff_t c = 1024;

  ptrdiff_t n = a * b * c;


  MyArray array(n);


  for (ptrdiff_t i = 0; i != n; ++i)

    array(i) = 1;


  ptrdiff_t sum1 = array.CalcSum();


  for (int i = 0; i != n; ++i)

    array[i] = 2;


  ptrdiff_t sum2 = array.CalcSum();


  ...

}

В этот раз release-версия аварийно завершится. Посмотрим на ассемблерный код.



Для корректного operator() компилятор вновь построил код с вызовом memset. Эта часть, как и раньше, отлично работает. А вот в коде, где используется operator[], происходит выход за рамки массива, так как условие "i != n" не выполняется. Это не совсем тот код, который я хотел создать, но в простом коде это очень сложно реализовать, а большой отрывок кода сложно рассматривать. В любом случае это не меняет сути. Код начал аварийно завершаться, как это и должно быть.


Почему я так много времени посвятил данной тематике? Видимо меня мучает проблема, что я не могу демонстрировать 64-битные ошибки на простых примерах. Я пишу что-то простое для демонстрации, но обидно, что если это действительно попробовать, то оно работает в release-варианте. А следовательно ошибки как бы и нет. Но они есть и очень коварны и сложны в обнаружении. Специально повторюсь. Подобные ошибки легко пропустить при отладке и прогоне юнит-тестов на debug версии. Редко у кого хватить терпения отлаживать программу или ждать тесты, когда они обрабатывают гигабайты. Release-версия может пройти большое серьезное тестирование. А следующая сборка, где что-то незначительно поправлено, или используется новая версия компилятора будет неработоспособна на большом объеме данных.


По поводу диагностики данной ошибки смотрите предыдущий пост, где описывается новое предупреждение V302.


Per informazioni più dettagliate sulle ottimizzazioni basate su compilatore, vedere il nostro Avviso sull'ottimizzazione.