Алексей Доля
"Открытые Системы"
Windows 2000 Magazine, #08/2002
Сегодня 64-разрядные процессоры используются, прежде всего, в серверах, ориентированных на корпоративные сети, в которых работают приложения, требующие значительных вычислительных ресурсов и исключительной надежности систем. Подобным приложениям для различных вычислений необходимы огромные объемы оперативной памяти (большие базы данных, системы принятия решений и бизнес-анализа, системы управления ресурсами предприятия, быстродействующие базы данных, хранящиеся в оперативной памяти, и т. д.), в основном они используются в банковской, производственной, инженерной и научно-исследовательской деятельности. Естественно, сервер, построенный на 64-разрядной платформе, стоит не один десяток тысяч долларов, что могут себе позволить только весьма крупные корпоративные заказчики. Да и адресное пространство свыше 4 Гбайт, необходимое для обработки сверхбольших СУБД, рядовому пользователю ни к чему.
Данная статья ориентирована на программистов, занимающихся разработкой программного обеспечения для 64-разрядной платформы. Мы рассмотрим преимущества этой платформы, недостатки использования 32-разрядного кода на 64-разрядной платформе, основы 64-разрядного программирования и способы перевода приложений Win32 на 64-разрядную платформу. Последние два вопроса освещены в современной прессе и MSDN достаточно слабо. Мы не будем акцентировать внимание на какой-либо конкретной модели (архитектуре) 64-разрядного процессора. Все рассмотренные в статье примеры используют язык С++ и компилируются с помощью 64-разрядного компилятора С++, входящего в состав последних дистрибутивов SDK и DDK.
Win64-код объединяет в себе основные возможности 32-разрядного кода, а также включает изменения, связанные с повышением разрядности. В распоряжении программиста оказываются:
Обратите внимание, что 32-разрядные типы данных не исчезли при повышении разрядности платформы (как было с 16-разрядными типами данных при переходе к Win32). Это связано с тем, что даже в 64-разрядных приложениях в большинстве случаев переменные не требуют объема памяти в 8 байт, поэтому использование 64-разрядных типов в таких случаях оказалось бы крайне неэффективным. Операционной системе пришлось бы дописывать нули в старшие разряды, чтобы увеличить размер данных до 8 байт (такие данные к тому же очень неудобно считывать). Это привело бы к снижению производительности.
Иная участь постигла 32-разрядные указатели: они полностью исчезли. Дело в том, что использование 32-разрядных указателей накладывает ограничение на объем адресуемой памяти. Например, одним из главных преимуществ плоской модели памяти (она является основной для программирования 32-разрядных приложений для платформы NT), использующей 32-разрядные указатели, является возможность создания сегментов объемом до 4 Гбайт. Новые 64-разрядные указатели обеспечивают возможность адресации до 16 Тбайт памяти (1 Тбайт = 1012 Мбайт). Современными бизнес-приложениями этот объем вполне востребован.
Функции в Win64 API претерпели незначительные изменения. Только названия некоторых из них были изменены так, чтобы отразить принадлежность к 64-разрядной платформе. В большинстве случаев изменениям подверглись лишь типы параметров, являющихся аргументами вызова функций. Все остальные преимущества (возможность отказаться от использования файлов подкачки и т. д.) связаны либо с увеличившимся объемом адресации, либо с новыми типами данных.
Если бы 32-разрядные приложения работали в 64-разрядной среде так же эффективно, как в <родной>, не было бы никакого смысла не только писать эту статью, но и создавать 64-разрядный компилятор. Действительно, производительность 32-разрядных приложений при работе на 64-разрядной платформе существенно снижается. Это связано с тем, что для запуска 32-разрядного приложения операционной системе приходится выполнять ряд подготовительных действий: увеличивать все 32-разрядные указатели до размера в 8 байт, преобразовывать вызовы API-функций, заменять типы 32-разрядных данных на 64-разрядные.
Здесь следует остановиться и подробнее рассмотреть вопрос о преобразовании данных. Win64 допускает использование и 32-, и 64-разрядных данных. Поэтому когда операционная система встречает 32-разрядные данные в 32-разрядном приложении, она должна ответить на вопрос: <Преобразовать ли эти данные в 64-разрядные или оставить, как есть?> Делается это для того, чтобы оптимизировать работу приложения. Если операционная система встретит 32-разрядные данные в 64-разрядном приложении, то не обратит на них внимания, так как платформа Win64 допускает использование 32-разрядных типов данных. Преобразование 32-разрядных данных в 64-разрядные осуществляется точно так же, как и преобразование 32-разрядных указателей, - дописыванием нулей в старшие разряды. Очевидно, что на выполнение всех этих операций тратится масса системных ресурсов, и, как результат, производительность снижается. Это особенно заметно при работе 32-разрядных драйверов на 64-разрядной платформе. В связи с тем, что драйверы являются связующим звеном между оборудованием и операционной системой, именно они используются наиболее интенсивно.
Для работы с 64-разрядным кодом понадобятся компилятор, линкер и несколько подключаемых библиотек. Все это есть в последних дистрибутивах SDK и DDK. Прежде всего, необходимо рассмотреть макросы и директивы компилятора, предназначенные для 64-разрядного кода:
Также компилятор имеет встроенные предопределенные макросы, специфичные для различных архитектур процессоров:
Следует с большой осторожностью пользоваться макросами, определяющими архитектуру процессора, так как на процессоре другой архитектуры приложение может не работать вообще. Напротив, макросы, определяющие платформу, надо использовать как можно чаще.
Теперь есть возможность создавать приложения, работающие и на 32-, и на 64-разрядной платформах. Здесь есть, однако, немало подводных камней. Следует быть очень внимательным - например, следующий код был вполне пригоден для <старых> приложений, но не для 64-разрядной платформы:
#ifdef _WIN32 // Win32 код ... #else // А здесь неясно, какой код следует далее: 64- или 16-разрядный... ... #endif И этот код не является корректным: #ifdef _WIN16 // Win16 код ... #else // То же самое: 32- или 64-разрядный код? ... #endif
Чтобы исправить этот код, нужно после #else добавить макросы _WIN32 или _WIN64. Просто придется привыкнуть к тому, что теперь код бывает трех (а не двух, как раньше) <видов>, поэтому использование директивы #else в привычных конструкциях приведет к неоднозначности (хотя 16-разрядных приложений становится все меньше и меньше, поддержка этой платформы в современных компиляторах остается).
Теперь рассмотрим новые типы данных. Их можно условно разделить на три группы (см. Таблицу 1):
Описания этих типов находятся в файле basetsd.h (входящем в состав DDK/SDK) и приведены в Таблице 1.
Теперь рассмотрим особенности новых типов данных. Каждый целочисленный тип явного представления имеет размер, соответствующий его определению. Это означает, что переменная типа int32 всегда будет занимать 32 разряда, вне зависимости от платформы, на которой запускается и исполняется код. Обратите внимание, что при следующем макросе
... #ifdef _WIN32 // Win32 код ...
под 32-разрядным кодом подразумевается использование 32-разрядных типов данных. Именно для переменных этой платформы служит тип int32. То же касается и других целочисленных типов явного представления, имеющих размер 32 разряда.
Второй вид (целочисленные типы, представленные указателями) имеет свою особенность: размер данных каждого такого типа зависит от платформы, на которой исполняется приложение. Именно поэтому многие из этих типов названы интегральными. Они сочетают в себе свойства 32- и 64-разрядных типов. Эти типы можно сравнить с виртуальными функциями и поздним связыванием из объектно-ориентированного программирования, так как в обоих случаях среда исполнения определяет конкретные свойства элементов только во время исполнения приложения, а не компиляции (в этом и заключается позднее связывание). Эти типы данных позволяют создавать приложения, одинаково хорошо работающие на 32- и 64-разрядной платформах.
Далее следуют указатели. При работе с ними надо соблюдать осторожность: необходимо учитывать разрядность данных, которые адресуются указателем, и помнить, что 32-разрядные указатели в 64-разрядном коде всегда расширяются операционной системой до нужного размера.
Следующие функции Win64 (Helper Functions) отвечают за преобразование одного типа в другой (эти inline-функции определены в Basetsd.h). Смысл многих из них понятен из определения прототипов, к остальным требуются объяснения:
Теперь рассмотрим изменения в Win64 API. Как уже отмечалось, лишь некоторые функции Win64 API были изменены. Для того чтобы перевести 32-разрядный код в 64-разрядный, следует использовать новые функции оконного класса (Window class). Если в private-данных окна или класса есть указатели, необходимо задействовать следующие новые функции: GetClassLongPtr, GetWindowLongPtr, SetClassLongPtr, SetWindowLongPtr. Эти функции могут работать и на 32- и на 64-разрядной платформе, но для компиляции требуют 64-разрядного компилятора.
Также необходимо, чтобы указатели и дескрипторы (handles), входящие в private-данные класса, могли использовать новые 64-разрядные функции. Нужно иметь в виду, что следующие элементы в Winuser.h во время 64-разрядной компиляции не определены:
GWL_WNDPROC, GWL_HINSTANCE, GWL_HWDPARENT, GWL_USERDATA.
Вместо них в Winuser.h определены следующие новые элементы:
GWLP_WNDPROC, GWLP_HINSTANCE, GWLP_HWNDPARENT, GWLP_USERDATA, GWLP_ID.
Например, следующий код вызовет ошибку компиляции:
SetWindowLong(hWnd, GWL_WNDPROC, (LONG)MyWndProc);
Его нужно изменить так:
SetWindowLongPtr(hWnd, GWLP_WNDPROC, (LONG_PTR)MyWndProc);
Обращаю внимание читателей на следующий момент. Прежде чем делать cbWndExtra членом структуры WNDCLASS, необходимо убедиться, что для указателя имеется достаточно памяти. Например, если зарезервировано sizeof(DWORD) байт для адресуемой переменной, следует зарезервировать sizeof(DWORD_PTR) байт.
Теперь вернемся к новым 64-разрядным API-функциям. Все они объявлены в Winuser.h, включены в Windows.h и используют User32.lib.
1. GetClassLongPtr. GetClassLongPtr возвращает значение из структуры WNDCLASSEX, которое связано с определенным окном. Если вы возвращаете указатель или дескриптор окна (handle), эта функция в точности повторяет GetClassLong. Но чтобы написать код, работающий под двумя платформами (32- и 64-разрядной), нужно использовать только GetClassLongPtr. Вот ее определение:
ULONG_PTR GetClassLongPtr( HWND hWnd, // указатель на окно int nIndex // смещение возвращаемого значения).
Было бы нецелесообразно описывать все аргументы, так как они в точности повторяют параметры 32-разрядной функции GetClassLong (и, следовательно, доступны в любой справке по Win32 API).
2. GetWindowLongPtr. Здесь та же картина, что и в предыдущем случае. Эта функция является заменой <старой> функции GetWindowLong и служит лишь для совместимости двух платформ (а значит, и для создания кросс-платформенных приложений). Функция GetWindowLongPtr возвращает дескриптор окна и значение из экстрапамяти окна по указанному смещению. Вот ее определение:
LONG_PTR GetWindowLongPtr( HWND hWnd, // указатель на окно int nIndex // смещение возвращаемого значения).
3. SetClassLongPtr. Функция SetClassLong заменяет определенные значения по заданным смещениям в экстрапамяти класса или WNDCLASSEX-структуры на значения того класса, к которому принадлежит данное окно. Во многом эта функция похожа на SetClassLong, но она является межплатформенной.
ULONG_PTR SetClassLongPtr( HWND hWnd, // указатель на окно int nIndex, // указатель на значение, которое надо поменять LONG_PTR dwNewLong // новое значение);
4. SetWindowLongPtr. Функция SetWindowLongPtr меняет атрибуты окон. Она также записывает определенные значения (по определенным смещениям) в экстрапамять окна. Эта функция является интегральным (в смысле объединения двух платформ) аналогом SetWindowLong.
LONG_PTR SetWindowLongPtr( HWND hWnd, // указатель на окно int nIndex, // смещение, куда записывать значение LONG_PTR dwNewLong // новое значение).
Итак, я рассказал обо всех программных средствах, предоставляемых Win64. Пора разобраться, как применить эти знания. В общем виде алгоритм перевода кода Win32 на 64-разрядную платформу выглядит так:
Для создания кросс-платформенных приложений (это предпочтительней первого варианта) необходимо:
К сожалению, пока не существует программ, которые могли бы помочь это сделать. Поэтому все изменения нужно делать самим. Единственным помощником в данном случае является 64-разрядный компилятор: в частности, его режим предупреждений (warnings), касающийся 64-разрядного кода. Для того чтобы включить эти предупреждения, нужно воспользоваться параметром компилятора-Wp64-W3. Он сделает активными следующие предупреждения:
Для преобразования кода в 64-разрядный нужно исправить все строки кода, на которые укажет компилятор. Некоторые советы приведены в таблице ниже.
Мы постарались по возможности рассмотреть все значимые аспекты перевода 32-разрядного кода на 64-разрядную платформу. Следует заметить, что портирование драйверов происходит точно по такому же принципу. Хотя при этом обнаруживается достаточно много подводных камней, каждый из которых проявляется в своем конкретном случае. Составить общее представление об этом вопросе позволяет MSDN.
Как сообщают представители Microsoft, средства для работы с 64-разрядным кодом (новые типы данных, указатели, компилятор, библиотеки...) будут включены в новую версию MS Visual C++. Пока что в нашем распоряжении есть DDK/SDK, и это уже немало. С их помощью уже сейчас можно создавать кросс-платформенные приложения и портировать 32-разрядный код.
АЛЕКСЕЙ ДОЛЯ - технический писатель. С ним можно связаться по адресу: TanaT@hotmail.ru.
При портировании 32-разрядного кода на 64-разрядную платформу следует учитывать следующие моменты:
Таблица 1.
Типы данных для 64-разрядного программирования.