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

Недавно наша команда закончила портирование одного довольно большого проекта (9 миллионов строк кода, 300 мегабайт исходных файлов) на 64-битную платформу. Нам потребовалось полтора года. Хотя NDA не разрешает нам раскрывать название проекта, мы все же надеемся, что наш опыт поможет другим разработчикам в их работе.

Об авторах

Многим мы известны как авторы статического анализатора кода PVS-Studio. Действительно, это наша основная сфера деятельности. Но мы также принимаем участие в различных сторонних проектах в качестве команды экспертов. Мы называем это экспертной продажей. Некоторое время назад мы опубликовали отчет о проделанной работе над Unreal Engine 4. Сегодня мы представляем очередной отчет о работе, которую мы проделали для нашей последней задачи по продаже экспертизы.

«Ага, тогда у них, должно быть, с PVS-Studio туго!» — могут подумать некоторые читатели, следящие за нашей активностью. Но приходится разочаровывать жаждущих сенсаций. Участие в сторонних проектах действительно очень важная деятельность для нашей команды, но по другой причине. Это способ более активно использовать собственный инструмент в реальной жизни, а не только при работе над его развитием. Ежедневное использование анализатора в реальных коммерческих проектах, разработанных десятками, а то и сотнями разработчиков, является источником большого опыта для команды PVS-Studio. Мы видим, как люди используют наш инструмент, с какими трудностями сталкиваются и что нам следует изменить или улучшить в нашем продукте.

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

Введение или в чем проблема? Сроки проекта и размер команды

На первый взгляд задача переноса программного кода на платформу x64 выглядит понятной и тривиальной. Еще в 2010 году мы написали нашу проверенную временем статью «Сборник примеров 64-битных ошибок в реальных программах». В 2012 году мы опубликовали наш обучающий курс Уроки разработки 64-битных C/C++ приложений. Все, что вам нужно сделать, это следовать приведенным там рекомендациям и указаниям, и все будет хорошо, верно? Тогда зачем заказчику нужно было обращаться к сторонней компании (т.е. к нам) и почему даже нам пришлось потратить на это полтора года? Поскольку мы реализуем анализ 64-битных проблем как часть функционала PVS-Studio, мы должны разбираться в теме, не так ли? Да, и мы определенно очень хорошо знаем свою тему, и это была основная причина, по которой заказчик обратился к нам. Но зачем им вообще было просить кого-то помочь с 64-битной миграцией их кода?

Сначала несколько слов о проекте и заказчике. Поскольку NDA не разрешает нам раскрывать информацию напрямую, я приведу лишь некоторые цифры. Проекту, над которым мы работали, около 20 лет. В настоящее время над ним ежедневно работает несколько десятков программистов. Их клиентами являются крупные компании, и их продажи редки, поскольку их продукт является узкоспециализированным.

И главная характеристика — это размер кода. Исходный код включает 9 миллионов строк кода и имеет общий размер, достигающий 300 Мбайт, файл решения (.sln) охватывает тысячи проектов, что является ОГРОМНЫМ числом. Целевой платформой является только Windows. Но даже в таком проекте не должно быть сложностей с задачей миграции на 64-бита, не так ли? Чтобы портировать такой проект на платформу x64, вам нужно всего лишь сделать следующее:

  • полностью остановить процесс разработки на несколько месяцев;
  • быстро заменить типы данных их 64-битными аналогами;
  • убедиться, что после замены все работает исправно;
  • возобновить процесс разработки.

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

Из-за некоторых бизнес-ограничений заказчик не мог остановить процесс разработки. Их собственные клиенты будут регулярно нуждаться в новых выпусках, исправлениях ошибок, специальных функциях и т. д. Остановить процесс разработки в подобных обстоятельствах означало бы полностью остановить бизнес. Поэтому они приступили к поиску команды, которая могла бы помочь портировать проект, не останавливая процесс разработки. Нас выбрали потому, что наша компетенция в 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-разрядного приложения.

Большинство этих правок фактически подразумевало замену 32-битных типов на memsize-типы, как и в первом подходе. Но, в отличие от него, на этот раз мы делали эти замены выборочно и итеративно. Это было обусловлено тем, что редактирование типов параметров функций приводило, в свою очередь, к редактированию типов локальных переменных и возвращаемых значений, которые, в свою очередь, приводили далее к редактированию типов параметров других функций, и так далее, пока кружок был закрыт.

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

При портировании приложения нам также понадобились 64-битные версии сторонних библиотек, используемых в проекте. Что касается библиотек с открытым исходным кодом, мы пытались собрать их из тех же исходных файлов, из которых были созданы 32-разрядные библиотеки. Причина заключалась в том, что мы хотели сохранить все возможные исправления ошибок, уже сделанные в их коде, если таковые имеются; и нам также нужно было по возможности создавать их в той же конфигурации, что и для 32-битных версий — например, с переключателем компиляции, указывающим компилятору не рассматривать wchar_t как встроенный тип, или с отключенной поддержкой Unicode. В таких случаях нам приходилось немного играть с параметрами построения, прежде чем мы могли понять, почему они не будут связаны с нашим проектом. Некоторые библиотеки просто не были предназначены для пересборки в 64-битной версии, и в этом случае нам приходилось либо конвертировать их самостоятельно, либо загружать более свежие версии, допускающие 64-битную сборку. В случае с коммерческими библиотеками мы либо просили заказчика приобрести 64-битную версию, либо искали замену тем из них, которые уже не имели поддержки, как это было с xaudio.

Нам также пришлось избавиться от всех ассемблерных вставок, так как 64-битная версия компилятора Visual C++ не поддерживает ассемблер. В этом случае мы либо использовали встроенные функции везде, где это возможно, либо переписывали код на 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.

Все приведенные выше примеры, а также многие другие типы 64-битных проблем можно найти с помощью анализатора PVS-Studio.

Роль PVS-Studio в 64-битной миграции

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

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

Вывод

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

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

Дополнительная литература по 64-разрядной миграции

Статья опубликована с разрешения автора.