>
>
Как перенести проект размером в 9 млн с…

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

Илья Иванов
Статей: 7

Как перенести проект размером в 9 млн строк кода на 64-битную платформу?

Недавно наша команда завершила миграцию на 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-битной миграции нужна, естественно, замена некоторых типов данных на 64-битные. Если в проекте такого размера создать отдельную ветку и внести там все необходимые правки, то объединить код (выполнить merge) будет невозможно! Не забывайте об объеме проекта и нескольких десятках программистов, которые ежедневно пишут новый код.

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

Мы выполнили миграцию за полтора года. С нашей стороны в проекте первые полгода принимали участие два человека, затем еще год уже четыре человека. Почему так долго? Первые полгода два человека занимались настройкой инфраструктуры, знакомством с проектом, проверкой конкретных способов миграции. Затем, через полгода, когда задача стала уже более конкретной, к проекту подключились еще люди и уже 4 человека за год выполнили миграцию.

Как перенести проект на 64-битную систему?

Перенос проекта на 64-битную платформу по большому счёту заключается в следующих двух шагах:

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

Напомним, что под 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-битной конфигурации.
  • Компиляция.
  • Замена большинства 32-битных типов на 64-битные (вернее на memsize-типы).
  • Линковка со сторонними библиотеками.
  • Запуск приложения.
  • Правка оставшихся предупреждений компилятора.
  • Правка оставшихся 64-битных ошибок, которые выявит анализатор PVS-Studio.

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

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

  • Создание 64-битной конфигурации.
  • Компиляция.
  • Линковка со сторонними библиотеками.
  • Запуск приложения.
  • Правка предупреждений компилятора.
  • Правка самых приоритетных 64-битных ошибок, которые выявит анализатор PVS-Studio.

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

Дальше нам предстояло поправить предупреждения компилятора и 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-битной версии и прогона тестов. Когда работа над первой частью проектов была закончена, мы смогли оценить, с какой скоростью мы работаем, по объёму портированного кода за определённый период.

Примеры 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-битных ошибок.

Роль статического анализатора PVS-Studio при 64-битной миграции

Часть потенциальных ошибок при миграции на 64-битную платформу находит компилятор и выдаёт соответствующие предупреждения. PVS-Studio лучше справляется с этой задачей, так как инструмент изначально разрабатывался с целью находить все такие ошибки. Более подробно о том, какие 64-битные ошибки находит PVS-Studio и не находят компилятор и статический анализатор Visual Studio, можно прочитать в статье "64-битный код в 2015 году: что нового в диагностике возможных проблем?".

Хочется обратить внимание ещё на один важный момент. Регулярно используя статический анализатор, мы могли постоянно наблюдать, как исчезают старые, а иногда добавляются новые 64-битные ошибки. Ведь код постоянно правят десятки программистов. И иногда они могут ошибиться и внести 64-битную ошибку в проект, который уже адаптирован к x64. Если бы не статический анализ, было бы невозможно сказать, сколько ошибок исправлено, сколько внесено, и на каком этапе мы сейчас находимся. Благодаря PVS-Studio мы строили графики, которые помогали нам иметь представление о прогрессе. Но это уже тема для отдельной статьи.

Заключение

Для того, чтобы 64-битная миграция вашего проекта прошла как можно более спокойно, последовательность шагов должна быть следующей:

  • Изучить теорию (например, наши статьи).
  • Найти все 64-битные библиотеки, которые используются в проекте.
  • Максимально быстро собрать 64-битную версию, которая компилируется и линкуется.
  • Исправить все 64-битные сообщения первого уровня анализатора PVS-Studio (64 L1).

Что почитать про 64-битную миграцию?