понедельник, 10 октября 2016 г.

А Вы перешли на WIN64? (Часть 2)

В предыдущей заметке я рассказывал о начале работ по адаптации наших приложений к платформе Win64. Эта заметка является продолжением.

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

Первое, на что стоит обратить внимание - это работа с указателями через Integer: в Win64 тип Pointer стал 64-битным (т.е. 8 байт), а тип Integer остался 32-битным (т.е. 4 байта). Подробно об этом писал Александр Алексеев: "Введение в 64 бита на Windows" (обязательно почитайте, если ещё не читали).
Суть проблемы в том, что в Win64 цепочка преобразований Pointer -> Integer -> Pointer (например, при сохранении ссылки на объект в свойство компонента Tag с приведением типа через Integer) приводит к искажению начального значения (верхние 4 байта срезаются, а ещё Integer - знаковое), соответственно можно получить AV, либо вылет в непредсказуемых местах. Проблема усугубляется тем, что проявится она только после того, как выделение памяти приложению перейдёт границу в 2 Гб, что при обычном использовании прикладных приложений - редкость, и воспроизвести такие ошибки будет очень трудно.
Поэтому, ещё до каких-либо исправлений в исходниках, я написал очень простой код:
const
  X = 6;
var
  L: array of Pointer;
  I: Integer;
begin
  SetLength(L, 1024 * 1024);
  for I := Low(L) to High(L) do
    GetMem(L[I], 1024 * X);
И разместил его в секции иницализации самого первого модуля наших приложений (обернув отдельным дефайном - в релизе такой радости никому не нужно). Код выделяет X Гб памяти при старте приложения ещё до инициализации сторонних библиотек (я указал X = 6 -- чтобы наверняка :), и это позволяет воспроизводить ошибки без проблем.
Далее тупо поиск и пересмотр кода (замена Integer на NativeInt). Слова для поиска были такими:
Integer(
Cardinal(
Longint(
Longword(
Tag
FFFFFFFF

Следующая проблема - из той же серии, но при работе уже с сообщениями Windows (а также другими вызовами Winapi-функций). Вместо Integer надо использовать виндовые WPARAM, LPARAM, LRESULT, HWND и т.п. Сюда же отнесу Get/SetWindowLong - при подмене оконной процедуры теперь надо использовать Get/SetWindowLongPtr.
Здесь список слов для поиска получился примерно таким:
Perform
PostMessage
SendMessage
EnumWindows
Long(

Самая рутинная проблема - это локальные (вложенные) процедуры, используемые как CallBack-процедуры, передаваемые по указателю. Пример подробно расписан у Александра Багеля: "Анализ задачи №18".
Если процедуру разместить как вложенную внутри метода, а затем передать её по указателю как CallBack-процедуру, то область видимости внутри процедуры в момент её исполнения будет совсем другой, нежели в момент компиляции - это может привести к ошибкам. Поэтому CallBack-процедуры должны быть самостоятельными (не вложенными).
Однако, размещая процедуру как вложенную в метод, мы ограничиваем её область видимости (в том плане, что на неё можно сослаться только из этого метода). Понятно, что внутри такой процедуры можно обращаться только к входным параметрам самой процедуры, и это прекрасно работает в Win32. В Win64 - увы, это уже не работает (как я это понимаю - это не проблема архитектуры Win64, а особенность реализации компилятора dcc64, но тем не менее).
Найти все такие места и поправить - не совсем тривиальная задача. Однако у нас выработана привычка: все такие процедуры должны содержать слово CallBack в своём имени. Поэтому я делал так: поиск по фразе CallBack, затем смотрю в какую процедуру это передаётся, затем поиск всех таких же процедур (для выявления случаев несоблюдения правила именования, а такие были).

Ну и конечно ассемблерный код (некоторые asm-процедуры могут скомпилироваться, но будут вылетать Win64).

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

P.S.:
  1. Время полной сборки приложений 64-битным компилятором существенно больше, по сравнению с 32-битным (16.1 секунд, против 4.7 для приложения в 1 098 192 строк кода). Хотя разница в простой перекомпиляции (когда правишь один-два модуля) на глаз не заметна.
  2. Размер 64-битных exe-файлов больше 32-битных (в среднем - на 40%), естественно и расход оперативной памяти тоже больше (в среднем - на 27%).
  3. Скорость работы с большими массивами данных особо не измерял, но там где измерял (сортировка и экспорт) получил прирост производительности примерно на 5%.
P.P.S.: Разобрался с SendMessage окну другого приложения: оно не работает, если это другое приложение запущено от имени администратора (и от отладчика это не зависит никак).

10 коммент.:

Александр Алексеев комментирует...

GetMem лучше заменить на VirtualAlloc MEM_RESERVE, тогда это можно и в релиз, т.к. "бесплатно".

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

Николай Зверев комментирует...

Мм.. насчёт в релиз и "бесплатно" - интересно.
Насчёт первых двух гигов - ценное замечание. Умом я это понимаю, но вопрос не исследовал. Мне хватило проверки, что после отработки приведённого куска по отхвату первых 6Гб встроенный менеджер памяти возвращает указатели уже за пределами 4Гб.
(Да, последовательные выделения возвращают адреса не по возрастанию... и я ещё вставлял ассерты чтобы следующие выделения были за пределами 4Гб и ассерты проходили..)

Александр Люлин комментирует...

Локальные callback'и я научился делать в 64 бита.

Александр Люлин комментирует...

Про преаллоцирование памяти - тоже допёр.

Александр Люлин комментирует...

Там через rcx надо подавать rbp от объемлющей процедуры.

Александр Люлин комментирует...

http://programmingmindstream.blogspot.com/2016/12/1323-win64.html?m=1

Александр Люлин комментирует...

http://programmingmindstream.blogspot.ru/2016/12/1321.html?m=1

Александр Люлин комментирует...

"Понятно, что внутри такой процедуры можно обращаться только к входным параметрам самой процедуры, и это прекрасно работает в Win32. В Win64 - увы, это уже не работает (как я это понимаю - это не проблема архитектуры Win64, а особенность реализации компилятора dcc64, но тем не менее)."

Там rcx - занят rbp. A остальные параметры - rdx,r8, r9

Александр Люлин комментирует...

http://programmingmindstream.blogspot.ru/2017/01/1333-64-integer.html

Александр Люлин комментирует...

Вот ещё, что попутно тут надумалось:
http://programmingmindstream.blogspot.com/2017/02/1346.html

Отправить комментарий

.

.