Для получения триального ключа
заполните форму ниже
Team License (базовая версия)
Enterprise License (расширенная версия)
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Запросите информацию о ценах
Новая лицензия
Продление лицензии
--Выберите валюту--
USD
EUR
RUB
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Бесплатная лицензия PVS-Studio для специалистов Microsoft MVP
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

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

Мне интересно попробовать плагин на:
* Нажимая на кнопку, вы даете согласие на обработку
своих персональных данных. См. Политику конфиденциальности

Ваше сообщение отправлено.

Мы ответим вам на


Если вы так и не получили ответ, пожалуйста, проверьте папку
Spam/Junk и нажмите на письме кнопку "Не спам".
Так Вы не пропустите ответы от нашей команды.

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

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

11 Янв 2010

В предыдущей записи блога я обещал рассказать, почему сложно демонстрировать 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);
}

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

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

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

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

0047_Optimization_in_the_world_of_64-bit_errors_ru/image1.png

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

Данная ошибка может быть легко обнаружена в 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-версия аварийно завершится. Посмотрим на ассемблерный код.

0047_Optimization_in_the_world_of_64-bit_errors_ru/image2.png

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

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

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

Популярные статьи по теме
Простая ошибка при кодировании - не значит нестрашная ошибка

Дата: 19 Апр 2017

Автор: Андрей Карпов

Популяризируя статический анализатор кода PVS-Studio, мы обычно пишем статьи для программистов. Однако, на некоторые вещи программисты смотрят одностороннее. Именно поэтому и существуют менеджеры про…
Как обнаружить переполнение 32-битных переменных в длинных циклах в 64-битной программе?

Дата: 22 Мар 2016

Автор: Андрей Карпов

Одна из проблем, с которой сталкиваются разработчики 64-битных приложений, это переполнение 32-битных переменных в очень длинных циклах. С этой задачей хорошо справляется анализатор кода PVS-Studio (…
64-битный код в 2015 году: что нового в диагностике возможных проблем?

Дата: 21 Май 2015

Автор: Сергей Васильев

64-битные ошибки достаточно тяжело обнаружить, так как они сродни бомбе замедленного действия: могут дать о себе знать далеко не сразу. Статический анализатор PVS-Studio облегчает задачу поиска и исп…
C++11 и 64-битные ошибки

Дата: 29 Апр 2014

Автор: Андрей Карпов

64-битные компьютеры давно и успешно используются. Большинство приложений стали 64-битными. Это позволяет им использовать больший объем памяти, а также получить прирост производительности за счёт арх…
Отличие %p от %x

Дата: 05 Апр 2013

Автор: Андрей Карпов

В функциях семейства printf существуют спецификаторы типа "%p" и "%x".

Комментарии (0)

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