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

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

![0342_BigProject-x64_ru/image1.png](https://import.viva64.com/docx/blog/0342_BigProject-x64_ru/image1.png)

## Об авторах

Многие знают нас как авторов статического анализатора кода [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/)\. Это действительно основная наша деятельность\. Однако кроме этого мы еще принимаем участие в сторонних проектах в качестве команды экспертов\. Мы называем это "продажа экспертизы"\. Недавно мы публиковали [отчет о работе над кодом Unreal Engine 4](https://pvs-studio.ru/ru/blog/posts/cpp/0330/)\. Сегодня время очередного отчета о проделанной работе в рамках продажи нашей экспертизы\.

"Ага, значит дела с PVS\-Studio у них идут совсем плохо\!", \- может воскликнуть читатель, который следит за нашей деятельностью\. Спешим огорчить охочую до сенсаций публику\. Участие в таких проектах очень важно для нашей команды, но совершенно по другой причине\. Таким образом мы можем сами более активно пользоваться нашим анализатором кода в реальной жизни, нежели просто при его разработке\. Реальное применение анализатора в коммерческих проектах, над которыми трудятся десятки или даже сотни программистов, дает потрясающий опыт команде PVS\-Studio\. Мы видим, как применяется наш инструмент, какие возникают сложности и что необходимо изменить или хотя бы просто улучшить в нашем продукте\.

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

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

На первый взгляд с темой миграции кода на платформу x64 все уже понятно\. Проверенная временем статья "[Коллекция примеров 64\-битных ошибок в реальных программах](https://pvs-studio.ru/ru/blog/posts/cpp/a0065/)" была написана нами в 2010 году\. Наш курс "[Уроки разработки 64\-битных приложений на языке Си/Си\+\+](https://pvs-studio.ru/ru/blog/lessons/)" \- в 2012 году\. Казалось бы, читай, делай как написано, и все будет хорошо\. Зачем же понадобилось заказчику обращаться к сторонней организации \(к нам\), и почему даже мы потратили на проект полтора года? Ведь если мы делаем в рамках PVS\-Studio анализ 64\-битных проблем, то вроде бы должны разбираться в теме? Конечно мы разбираемся, и это была главная причина того, что заказчик обратился к нам\. Но почему у заказчика вообще появилась мысль обратиться к кому\-то по поводу 64\-битной миграции?

Давайте сначала опишем проект и заказчика\. Так как NDA запрещает говорить напрямую, то приведем лишь количественные характеристики\. Проекту, над которым мы работали, порядка 20 лет\. В настоящее время над ним ежедневно работают несколько десятков разработчиков\. Клиенты \- крупные компании, продажи единичны, так как продукт очень нишевый и узкоспециализированный\. 

Ну и самое главное \- это размер\. 9 млн строк кода, 300Mb исходного кода, тысяча проектов в решении \(\.sln\) это ОЧЕНЬ много\. Платформа \- Windows only\. Но даже с таким проектом 64\-битная миграция вроде бы должна быть понятной\.  Для того, чтобы перевести такой проект на x64 надо всего лишь:

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

Почему первым пунктом указано "полностью остановить разработку"? Да потому что для 64\-битной миграции нужна, естественно, замена некоторых типов данных на 64\-битные\. Если в проекте такого размера создать отдельную ветку и внести там все необходимые правки, то объединить код \(выполнить merge\) будет невозможно\! Не забывайте об объеме проекта и нескольких десятках программистов, которые ежедневно пишут новый код\.

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

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

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

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

1. Создание 64\-битной конфигурации, получение 64\-битных версий сторонних библиотек и сборка проекта\.
1. Исправление кода, который приводит к ошибкам в 64\-битной версии\. Этот пункт почти полностью сводится к тому, что нужно заменить 32\-битные типы на [memsize\-типы](https://pvs-studio.ru/ru/blog/terms/0030/) в коде программы\.

Напомним, что под memsize\-типами понимают типы переменной размерности\. Такие типы имеют размер 4 байта на 32\-битной системе и 8 байт на 64\-битной\.

Портирование большого и активно развивающегося проекта не должно мешать текущей разработке, поэтому мы предприняли следующие меры\. Во\-первых, мы делали все наши правки в отдельной ветке, чтобы не ломать основную сборку\. Когда очередной набор наших изменений был готов и протестирован, мы объединяли наши изменения с основной веткой\. А во\-вторых, мы не стали менять жёстко 32\-битные типы на memsize\-типы\. Мы ввели свои типы и делали замену на них\. Это было сделано для того, чтобы избежать потенциальных проблем, таких как, например, вызов другой реализации перегруженной функции, а также, чтобы иметь возможность быстро откатить наши изменения\. Типы были введены приблизительно таким образом:

```cpp
    #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\. Хотя такой способ работает для небольших проектов, в нашем случае он не подошёл\. Во\-первых, замена типов занимала слишком много времени и приводила к большому количеству изменений\. А во\-вторых, как мы ни старались делать это аккуратно, мы, тем не менее, правили по ошибке структуры с форматами данных\. В результате, когда мы закончили работать над первой частью проектов и запустили приложение, мы не смогли загрузить предустановленные шаблоны интерфейсов, так как они были бинарными\.

Итак, первый план предполагал следующую последовательность действий\.

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

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

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

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

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

Дальше нам предстояло поправить предупреждения компилятора и 64\-битные предупреждения PVS\-Studio, чтобы устранить найденные и потенциальные падения\. Так как общее количество 64\-битных предупреждений PVS\-Studio исчислялась тысячами, то мы решили исправить только самые основные: неявные преобразования memsize\-типов к 32\-битным типам \(V103, V107, V110\), преобразования указателей к 32\-битным типам и наоборот \(V204, V205\), подозрительные цепочки преобразований \(V220, V221\), приведение в соответствие типов в параметрах виртуальных функций \(V301\) и замена устаревших функций на новые версии \(V303\)\. Описание всех этих диагностик вы можете найти в [документации](https://pvs-studio.ru/ru/docs/)\.

Другими словами, задача этого этапа \- исправить все 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\-тип\. Пример кода:

```cpp
MMRESULT m_tmScroll = timeSetEvent(
  GetScrollDelay(), TIMERRESOLUTION, TimerProc, 
  (DWORD)this, TIME_CALLBACK_FUNCTION);
```

Также проявились ошибки при изменении параметров виртуальных функций в базовом классе\. Например, у CWnd::OnTimer\(UINT\_PTR nIDEvent\) тип параметра поменялся с UINT на UINT\_PTR с появлением 64\-битной версии Windows, и соответственно во всех наследниках в нашем проекте нам тоже надо было выполнить эту замену\. Пример кода:

```cpp
class CConversionDlg : public CDialog {
...
public:
  afx_msg void OnTimer(UINT nIDEvent);
...
}
```

Некоторые WinAPI фукции поддерживают работу с большими объёмами данных, как, например, CreateFileMapping и MapViewOfFile\. И мы адаптировали код соответствующим образом:

Было:

```cpp
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());
```

Стало:

```cpp
#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 году: что нового в диагностике возможных проблем?](https://pvs-studio.ru/ru/blog/posts/cpp/0325/)"\.

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

## Заключение

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

1. Изучить теорию \(например, наши статьи\)\.
1. Найти все 64\-битные библиотеки, которые используются в проекте\.
1. Максимально быстро собрать 64\-битную версию, которая компилируется и линкуется\.
1. Исправить все 64\-битные сообщения первого уровня анализатора [PVS\-Studio](https://pvs-studio.ru/ru/pvs-studio/) \(64 L1\)\.

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

1. [Коллекция примеров 64\-битных ошибок в реальных программах](https://pvs-studio.ru/ru/blog/posts/cpp/a0065/)\.
1. [Уроки разработки 64\-битных приложений на языке Си/Си\+\+](https://pvs-studio.ru/ru/blog/lessons/)\.
1. [C\+\+11 и 64\-битные ошибки](https://pvs-studio.ru/ru/blog/posts/cpp/0253/)
1. [64\-битный код в 2015 году: что нового в диагностике возможных проблем?](https://pvs-studio.ru/ru/blog/posts/cpp/0325/)