Недавно наша команда завершила миграцию на 64-битную платформу довольного большого проекта (9 млн строк кода, 300Mb исходников). Проект занял полтора года. Хотя в силу NDA мы не можем привести название проекта, надеемся, что наш опыт окажется полезен другим разработчикам.
Многие знают нас как авторов статического анализатора кода PVS-Studio. Это действительно основная наша деятельность. Однако кроме этого мы еще принимаем участие в сторонних проектах в качестве команды экспертов. Мы называем это "продажа экспертизы". Недавно мы публиковали отчет о работе над кодом Unreal Engine 4. Сегодня время очередного отчета о проделанной работе в рамках продажи нашей экспертизы.
"Ага, значит дела с PVS-Studio у них идут совсем плохо!", - может воскликнуть читатель, который следит за нашей деятельностью. Спешим огорчить охочую до сенсаций публику. Участие в таких проектах очень важно для нашей команды, но совершенно по другой причине. Таким образом мы можем сами более активно пользоваться нашим анализатором кода в реальной жизни, нежели просто при его разработке. Реальное применение анализатора в коммерческих проектах, над которыми трудятся десятки или даже сотни программистов, дает потрясающий опыт команде PVS-Studio. Мы видим, как применяется наш инструмент, какие возникают сложности и что необходимо изменить или хотя бы просто улучшить в нашем продукте.
Поэтому мы и дальше планируем участвовать в таких проектах в рамках продажи нашей экспертизы. Пишите, если у вас есть подходящий для нас проект. А пока рады представить отчет о миграции кода на 64-битную платформу.
На первый взгляд с темой миграции кода на платформу x64 все уже понятно. Проверенная временем статья "Коллекция примеров 64-битных ошибок в реальных программах" была написана нами в 2010 году. Наш курс "Уроки разработки 64-битных приложений на языке Си/Си++" - в 2012 году. Казалось бы, читай, делай как написано, и все будет хорошо. Зачем же понадобилось заказчику обращаться к сторонней организации (к нам), и почему даже мы потратили на проект полтора года? Ведь если мы делаем в рамках PVS-Studio анализ 64-битных проблем, то вроде бы должны разбираться в теме? Конечно мы разбираемся, и это была главная причина того, что заказчик обратился к нам. Но почему у заказчика вообще появилась мысль обратиться к кому-то по поводу 64-битной миграции?
Давайте сначала опишем проект и заказчика. Так как NDA запрещает говорить напрямую, то приведем лишь количественные характеристики. Проекту, над которым мы работали, порядка 20 лет. В настоящее время над ним ежедневно работают несколько десятков разработчиков. Клиенты - крупные компании, продажи единичны, так как продукт очень нишевый и узкоспециализированный.
Ну и самое главное - это размер. 9 млн строк кода, 300Mb исходного кода, тысяча проектов в решении (.sln) это ОЧЕНЬ много. Платформа - Windows only. Но даже с таким проектом 64-битная миграция вроде бы должна быть понятной. Для того, чтобы перевести такой проект на x64 надо всего лишь:
Почему первым пунктом указано "полностью остановить разработку"? Да потому что для 64-битной миграции нужна, естественно, замена некоторых типов данных на 64-битные. Если в проекте такого размера создать отдельную ветку и внести там все необходимые правки, то объединить код (выполнить merge) будет невозможно! Не забывайте об объеме проекта и нескольких десятках программистов, которые ежедневно пишут новый код.
В силу бизнес-ограничений заказчик не мог остановить процесс разработки. Его клиентам постоянно нужны новые релизы, исправления проблем, специальные возможности и т.п. Остановить разработку в таких условиях означает остановить бизнес. Поэтому заказчик стал искать команду, которая может выполнить миграцию без остановки процесса разработки. Такой командой стали мы, так как наша компетенция в 64-битной разработке подтверждается анализатором кода PVS-Studio и статьями по данной тематике.
Мы выполнили миграцию за полтора года. С нашей стороны в проекте первые полгода принимали участие два человека, затем еще год уже четыре человека. Почему так долго? Первые полгода два человека занимались настройкой инфраструктуры, знакомством с проектом, проверкой конкретных способов миграции. Затем, через полгода, когда задача стала уже более конкретной, к проекту подключились еще люди и уже 4 человека за год выполнили миграцию.
Перенос проекта на 64-битную платформу по большому счёту заключается в следующих двух шагах:
Напомним, что под memsize-типами понимают типы переменной размерности. Такие типы имеют размер 4 байта на 32-битной системе и 8 байт на 64-битной.
Портирование большого и активно развивающегося проекта не должно мешать текущей разработке, поэтому мы предприняли следующие меры. Во-первых, мы делали все наши правки в отдельной ветке, чтобы не ломать основную сборку. Когда очередной набор наших изменений был готов и протестирован, мы объединяли наши изменения с основной веткой. А во-вторых, мы не стали менять жёстко 32-битные типы на memsize-типы. Мы ввели свои типы и делали замену на них. Это было сделано для того, чтобы избежать потенциальных проблем, таких как, например, вызов другой реализации перегруженной функции, а также, чтобы иметь возможность быстро откатить наши изменения. Типы были введены приблизительно таким образом:
#if defined(_M_IX86)
typedef long MyLong;
typedef unsigned long MyULong;
#elif defined(_M_X64)
typedef ptrdiff_t MyLong;
typedef size_t MyULong;
#else
#error "Unsupported build platform"
#endif
Хотим еще раз подчеркнуть, что мы меняли типы не на size_t/ptrdiff_t и им подобные типы, а на свои собственные типы данных. Это дало большую гибкость и возможность легко отследить те места, которые уже портированы, от тех, где пока "не ступала нога человека".
Первая идея портирования проекта была следующей: сначала заменить все 32-битные типы на memsize-типы за исключением тех мест, где 32-битные типы было необходимо оставить (например, это структуры, представляющие собой форматы данных, функции, обрабатывающие такие структуры), а потом уже привести проект в рабочее состояние. Мы решили сделать так для того, чтобы сразу устранить как можно больше 64-битных ошибок и сделать это за один проход, а потом доправить все оставшиеся предупреждения компилятора и PVS-Studio. Хотя такой способ работает для небольших проектов, в нашем случае он не подошёл. Во-первых, замена типов занимала слишком много времени и приводила к большому количеству изменений. А во-вторых, как мы ни старались делать это аккуратно, мы, тем не менее, правили по ошибке структуры с форматами данных. В результате, когда мы закончили работать над первой частью проектов и запустили приложение, мы не смогли загрузить предустановленные шаблоны интерфейсов, так как они были бинарными.
Итак, первый план предполагал следующую последовательность действий.
И этот план был признан неудачным. Мы выполнили первые пять пунктов, и все наши изменения в исходном коде пришлось откатить. Мы потратили впустую несколько месяцев работы.
Теперь мы решили сначала как можно быстрее получить рабочую 64-битную версию приложения, а потом уже поправить явные 64-битные ошибки. Наш план теперь исключал массовую замену типов и предполагал правку только явных 64-битных ошибок:
На этот раз мы получили первую рабочую версию приложения значительно быстрее, в том числе и потому что сторонние библиотеки уже были собраны, и шаблоны интерфейсов загрузились правильно. Надо сказать, что приложение в основном работало достаточно стабильно, что нас удивило. Мы нашли всего несколько падений при первом тестировании.
Дальше нам предстояло поправить предупреждения компилятора и 64-битные предупреждения PVS-Studio, чтобы устранить найденные и потенциальные падения. Так как общее количество 64-битных предупреждений PVS-Studio исчислялась тысячами, то мы решили исправить только самые основные: неявные преобразования memsize-типов к 32-битным типам (V103, V107, V110), преобразования указателей к 32-битным типам и наоборот (V204, V205), подозрительные цепочки преобразований (V220, V221), приведение в соответствие типов в параметрах виртуальных функций (V301) и замена устаревших функций на новые версии (V303). Описание всех этих диагностик вы можете найти в документации.
Другими словами, задача этого этапа - исправить все 64-битные сообщения PVS-Studio только первого уровня (level 1). Это наиболее важные диагностики. И для запуска 64-битного приложения все ошибки 64 L1 должны быть исправлены.
Большинство таких правок сводилось к замене 32-битных типов на memsize-типы, как и при первом подходе. Но в этот раз, в отличие от первого подхода, эти замены носили выборочный и итеративный характер. Это было связано с тем, что правка типов параметров функции тянула за собой правки типов локальных переменных и возвращаемого значения, которые в свою очередь приводили к правке типов параметров других функций. И так до тех пор, пока процесс не сошёлся.
Ещё один минус этого подхода в сравнении с первым заключается в том, что мы таким образом поправили только основные 64-битные ошибки. Например, типы счётчиков циклов мы не правили. В подавляющем большинстве случаев это было не нужно. И это не приводит к ошибкам, но возможно где-то это нужно было сделать, и такие места мы пропустили и не найдём при нашем подходе. Другими словами, возможно со временем еще кое-что придется поправить.
При портировании приложения нам также потребовалось получить 64-битные версии сторонних библиотек. В случае библиотек с открытым исходным кодом мы старались собрать их из тех же исходников, из которых были собраны 32-битные версии. Это было связано с тем, что мы хотели сохранить возможные правки в коде сторонних библиотек, если такие были, и также нам нужно было собирать их по возможности в такой же конфигурации, в какой они были собраны для 32-битной версии. Например, некоторые библиотеки были собраны с настройкой не считать wchar_t встроенным типом или с отключенной поддержкой юникода. В таких случаях нам пришлось немного повозиться с параметрами сборки, прежде чем мы смогли понять, почему наш проект не может слинковаться с ними. Какие-то библиотеки не предполагали сборку под 64-битную версию. И в этом случае нам приходилось либо самим конвертировать их, либо скачивать более новую версию с возможностью сборки под 64-битную платформу. В случае коммерческих библиотек мы либо просили докупить 64-битную версию, либо искали замену не поддерживающимся больше библиотекам, как в случае с xaudio.
Также нам нужно было избавиться от всех ассемблерных вставок, так как в 64-битной версии компилятора Visual C++ ассемблер не поддерживается. В этом случае мы либо использовали intrinsic функции там, где это можно было сделать, либо переписывали код на C++. В некоторых случаях это не приводило к ухудшению производительности, например, если в 32-битном ассемблерном коде использовались 64-битные MMX регистры, то в 64-битной версии у нас и так все регистры 64-битные.
В начале работы над большим проектом сложно сказать, сколько времени займёт портирование. Существенное время на первом этапе у нас заняла сборка сторонних библиотек, настройка окружения для ежедневной сборки 64-битной версии и прогона тестов. Когда работа над первой частью проектов была закончена, мы смогли оценить, с какой скоростью мы работаем, по объёму портированного кода за определённый период.
Самой распространённой ошибкой при портировании на 64-битную платформу было явное приведение указателей к 32-битным типам, например, к DWORD. В таких случаях решением была замена на memsize-тип. Пример кода:
MMRESULT m_tmScroll = timeSetEvent(
GetScrollDelay(), TIMERRESOLUTION, TimerProc,
(DWORD)this, TIME_CALLBACK_FUNCTION);
Также проявились ошибки при изменении параметров виртуальных функций в базовом классе. Например, у CWnd::OnTimer(UINT_PTR nIDEvent) тип параметра поменялся с UINT на UINT_PTR с появлением 64-битной версии Windows, и соответственно во всех наследниках в нашем проекте нам тоже надо было выполнить эту замену. Пример кода:
class CConversionDlg : public CDialog {
...
public:
afx_msg void OnTimer(UINT nIDEvent);
...
}
Некоторые WinAPI фукции поддерживают работу с большими объёмами данных, как, например, CreateFileMapping и MapViewOfFile. И мы адаптировали код соответствующим образом:
Было:
sharedMemory_ = ::CreateFileMapping(
INVALID_HANDLE_VALUE, // specify shared memory file
pSecurityAttributes, //NULL, // security attributes
PAGE_READWRITE, // sharing
NULL, // high-order DWORD of the file size
sharedMemorySize, // low-order DWORD of the file size
sharedMemoryName_.c_str());
Стало:
#if defined(_M_IX86)
DWORD sharedMemorySizeHigh = 0;
DWORD sharedMemorySizeLow = sharedMemorySize;
#elif defined(_M_X64)
ULARGE_INTEGER converter;
converter.QuadPart = sharedMemorySize;
DWORD sharedMemorySizeHigh = converter.HighPart;
DWORD sharedMemorySizeLow = converter.LowPart;
#else
#error "Unsuported build platform"
#endif
sharedMemory_ = ::CreateFileMapping(
INVALID_HANDLE_VALUE, // specify shared memory file
pSecurityAttributes, //NULL, // security attributes
PAGE_READWRITE, // sharing
sharedMemorySizeHigh, // high-order DWORD of the file size
sharedMemorySizeLow, // low-order DWORD of the file size
sharedMemoryName_.c_str());
Ещё в проекте нашлись места использования функций, которые в 64-битной версии считаются устаревшими и должны быть заменены на соответствующие новые реализации. Например, GetWindowLong/SetWindowLong следует заменить на GetWindowLongPtr/SetWindowLongPtr.
PVS-Studio находит все приведённые примеры и другие виды 64-битных ошибок.
Часть потенциальных ошибок при миграции на 64-битную платформу находит компилятор и выдаёт соответствующие предупреждения. PVS-Studio лучше справляется с этой задачей, так как инструмент изначально разрабатывался с целью находить все такие ошибки. Более подробно о том, какие 64-битные ошибки находит PVS-Studio и не находят компилятор и статический анализатор Visual Studio, можно прочитать в статье "64-битный код в 2015 году: что нового в диагностике возможных проблем?".
Хочется обратить внимание ещё на один важный момент. Регулярно используя статический анализатор, мы могли постоянно наблюдать, как исчезают старые, а иногда добавляются новые 64-битные ошибки. Ведь код постоянно правят десятки программистов. И иногда они могут ошибиться и внести 64-битную ошибку в проект, который уже адаптирован к x64. Если бы не статический анализ, было бы невозможно сказать, сколько ошибок исправлено, сколько внесено, и на каком этапе мы сейчас находимся. Благодаря PVS-Studio мы строили графики, которые помогали нам иметь представление о прогрессе. Но это уже тема для отдельной статьи.
Для того, чтобы 64-битная миграция вашего проекта прошла как можно более спокойно, последовательность шагов должна быть следующей: