>
>
>
Особенности разработки 64-битных прилож…

Евгений Рыжков
Статей: 125

Особенности разработки 64-битных приложений

Что же получили программисты с приходом 64-битных систем? Помимо многочисленных преимуществ, о которых написаны сотни и тысячи рекламных статей, программисты получили целый набор задачек, головоломок и даже ловушек, с которыми предстоит столкнуться всем, кто хочет получить реальные преимущества от переноса кода на 64-битные системы.

Введение

Традиционные 32-битные приложения уже начинают достигать предела своих возможностей по использованию оперативной памяти. В Windows-системах для пользовательского приложения доступно два гигабайта памяти (в некоторых случаях три), в то время как для повышения производительности программ в оперативной памяти уже необходимо хранить больше данных. Так, ограничения на память заставляют любителей компьютерных игр все чаще и чаще ждать дозагрузки частей одного уровня, что значительно снижает "эффект присутствия". Пользователи, обрабатывающие видеоизображения, вынуждены обрабатывать кадры с жесткого диска, вместо того, чтобы хранить все данные в быстрой оперативной памяти. Наконец, ученые для своей научной работы (моделирование, визуализация) должны ограничиваться областями минимального размера. Ведь моделировать области большой размерности со скоростью, адекватной задаче, можно лишь храня данные в оперативной памяти машины. И это уже не говоря о задачах, для решения которых необходимо использовать базы данных!

Наметившийся в конце прошлого века кризис в программном мире необходимо было как-то разрешить. Из истории человечества известны два способа развития: эволюционный и революционный. Разумеется, все ждут революции, которая позволит программистам не заботиться о размере оперативной памяти, скорости расчета и других вещах, игнорирование которых приводит к программам-монстрам. Однако, дата следующей компьютерной революции пока неизвестна (во всяком случае, автору данной статьи), а проблему решать надо уже сегодня (точнее даже "вчера"). Властелины компьютерного мира в лице компаний AMD и Intel предложили эволюционное увеличение разрядности машины. Вместо 32-битной архитектуры нам предложили 64-битную. Другими словами, для адресации ячейки оперативной памяти используются не 32-битные числа, а 64-битные. Это приводит к увеличению количества максимально доступной памяти до немыслимых пока значений. Такой вариант развития не является новым в компьютерном мире. Программисты постарше застали переход от 16-битного программного обеспечения к 32-битному, который начался при появлении процессора Intel 80386. Инженеры AMD и Intel решили повторить успех прошлого, увеличив адресное пространство и количество регистров процессора. В результате, проблемы современных компьютеров хоть и не были решены принципиально, но, в тоже время, необходимость их срочного решения была отложена.

64 бита для программистов: укрощение программ

Что же получили программисты с приходом 64-битных систем? Помимо многочисленных преимуществ, о которых написаны сотни и тысячи рекламных статей, программисты получили целый набор задачек, головоломок и даже ловушек, с которыми предстоит столкнуться всем, кто хочет получить реальные преимущества от переноса кода на 64-битные системы.

Когда мы говорим о реальных преимуществах, прежде всего мы имеем в виду доступную оперативную память. Если программа может использовать 64-битное адресное пространство, то это не значит, что конкретная программа написана именно таким образом. Что означает последняя фраза? Всего лишь то, что программа должна быть корректно написана (или перенесена с 32 бит) с учетом поддержки 64-битных систем.

Крупные производители средств разработки пытаются в определенной степени облегчить труд программистов за счет нахождения компилятором некоторых ошибок, связанных с использованием 64 бит. Основная часть доступной документации, выпускаемой этими производителями, утверждает, что, за редким исключением, перекомпиляции и исправления найденных средством разработки ошибок будет достаточно для правильной работы приложения под 64-битной системой. Однако практика показывает, что такие "автоматически отлавливаемые" ошибки - это только надводная часть айсберга, и, в действительности, проблем при переносе возникает намного больше.

Обратимся к конкретным примерам, которые нельзя найти в официальной документации к средствам разработки. Для хранения размеров блоков памяти, количества элементов массива и других подобных вещей в языке C++ используется специальный тип данных size_t. Размер этого типа совпадает с разрядностью процессора, т.е. на 32-битных системах его размер составляет 4 байта, а на 64-битных - 8 байт. Соответственно, на 32-битных системах максимально мы можем получить (теоретически) блок памяти размером в четыре миллиарда ячеек, а на 64-битных системах - значительно больший блок памяти. Казалось бы, программа автоматически получает преимущества 64-битных приложений сразу же после перекомпиляции. Но дьявол кроется в деталях. Везде ли в Ваших программах при работе с большими массивами и блоками памяти Вы используете тип size_t? Не было ли такого, что при написании кода для 32-битных систем, Вы рассуждали так: "Этот блок памяти уж точно никогда не будет больше гигабайта"? Если такое было, то вполне возможно, что для хранения размера блока памяти Вы использовали переменную типа int. Но ведь размер этой переменной даже на 64-битных системах равен 4 байтам! В результате, несмотря на то, что на 64-битной системе Вы могли бы выделить любое количество памяти для этого блока, фактически же Вы будете ограничены 4 гигабайтами. Это происходит из-за неверно выбранного типа переменной, в которой хранится размер блока памяти.

Но предположим, в Вашей программе размеры блоков памяти вычисляются верно. В таком случае выделяться память будет действительно большого размера, однако приложение все равно может оказаться неработоспособным. Почему такое может быть, если для хранения количества элементов массива мы используем переменную типа size_t? Давайте посмотрим на простейший цикл, в котором массив из 5 миллиардов элементов заполняется числами от 1 до 5000000000:

  size_t maxSize = 5000000000;
  int *buffer = new int[maxSize];
  size_t count = 0;
  for (int i = 0; i < maxSize; ++i) {
    buffer[i] = i;
  }     
  // ...
  delete[] buffer;

Если бы массив имел размер не 5 миллиардов, а 5 миллионов элементов, то данный код был бы корректен и на 32-битной, и на 64-битной системе. Однако 5 миллиардов элементов на 32-битной системе в оперативной памяти не поместятся. Но у нас же 64-битная система, поэтому для нас это не проблема, не так ли? Не так! В данном фрагменте переменная maxSize на 64-битной системе является 64-битной. Но счетчик цикла i (int) так и остался 32-битным. В результате значение переменной i будет изменяться от 0 до ... -2147483648 (минус два миллиарда)! Такой неожиданный эффект происходит из-за переполнения переменной. Будет ли при этом корректно заполнен наш массив? Вместо теоретических рассуждений о представлении данных в компьютере проведем эксперимент. Изменим код так:

size_t maxSize = 5000000000;
size_t count = 0;
for (int i = 0; i < maxSize; ++i) {
  count++;
}

После завершения цикла посмотрим на значение переменной count. Оно окажется равным... 2147483648. Вместо 5 миллиардов раз наш цикл выполнился всего лишь 2 миллиарда. В случае заполнения массива в цикле более чем половина элементов останутся неинициализированными!

В чем же проблема с такими конструкциями? Дело в том, что компиляторы не выдают диагностических сообщений на подобный код, поскольку с точки зрения С++ он написан корректно: переменная i преобразуется к типу size_t. Но мы-то хотели другого поведения программы. Помочь в диагностике подобных ошибок могут статические анализаторы кода, ориентированные на поиск проблем, связанных с переносом на 64-битные системы.

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

int Calc(size_t size) {
  // ...
}

Если вызывать эту функцию с аргументом типа int, то выполнится приведение типа и никаких диагностических сообщений компилятор не выдаст. Однако фактически получится изменение определения функции. Функция определена для всех чисел типа size_t, а фактически будет вызываться лишь для чисел типа int. Получается та же неприятная ситуация: код 64-битный, но реально используются лишь 32-битные числа.

Есть и более интересные ошибки в коде, которые могут привести к неожиданному поведению в программах, перенесенных из 32-битной среды в 64-битную. Например, в приложении может сломаться подсистема справки. Как связана справка с 64-битным кодом? Никак. Но вот с какой ситуацией пришлось столкнуться автору. Обычное Windows-приложение было написано на Visual C++ с использованием библиотеки MFC. Эта библиотека заслуженно пользуется уважением у разработчиков, так как позволяет легко создать каркас приложения без лишних трудностей, в том числе и добавить поддержку справочной системы. Для этого нужно всего лишь в классе приложения перекрыть виртуальную функцию WinHelp(). При этом иерархия наследования в Visual C++ 6.0 выглядела примерно так:

class CWinApp {
  virtual void WinHelp(DWORD dwData, UINT nCmd);
};
class CMyApp : public CWinApp {
  virtual void WinHelp(DWORD dwData, UINT nCmd);
};

В последующих версиях Visual C++ для поддержки 64-битного кода аргумент функции WinHelp() в библиотеке MFC был изменен с типа DWORD на тип DWORD_PTR:

class CWinApp {
  virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
}

Но в пользовательском коде изменений не производилось. В результате при компиляции кода для 64-битной платформы получилась не одна перекрытая виртуальная функция, а две независимые виртуальные функции, что привело к неработоспособности справочной системы. Для исправления ситуации нужно было исправить пользовательский код таким образом:

 class CMyApp : public CWinApp {
  virtual void WinHelp(DWORD_PTR dwData, UINT nCmd);
};

После чего справочная система заработала снова.

Заключение

В данной статье перечислены далеко не все проблемы, с которыми сталкиваются программисты после того, как перекомпилируют свои приложения для работы на 64-битных системах. Остались неосвещенными проблемы взаимодействия 32-битных и 64-битных приложений, поддержки сохранения и восстановления данных на системах с различной разрядностью, выбор компилятором неправильных перегруженных функций. Все эти проблемы имеют общие черты:

  • они появляются при переносе старых или разработке новых приложений на 64-битную платформу;
  • большинство из них не диагностируются компилятором, так как с точки зрения языка С++ код выглядит корректно;
  • подобные проблемы могут сильно снизить впечатление от 64-битной версии Вашего приложения.

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