Привет! Меня зовут Колосов Денис, я являюсь разработчиком клиентской части проекта «Allods Online» в студии IT Territory. Сегодня я расскажу о том, как мы решились обновить среду разработки и заодно компилятор на нашем проекте с Visual C++ 2010 на 2019. 

О чем пойдет речь?

  1. Как мы докатились до такой жизни и отважились на этот шаг;

  2. О сборке вендерских библиотек и всего окружения, которое у нас есть;

  3. С какими кастомными проблемами мы столкнулись;

  4. К чему это все привело.

Что касается сборки вендерских библиотек, затрону буквально в нескольких предложениях и больше возвращаться к теме не буду, т. к. в этом плане нам повезло. Во-первых, добрая их половина у нас есть в исходниках и лежит в отдельной папке в репозитории. 90% этих вендоров собираются прямо из solutions — они спокойно конвертируются из 2010 версии в 2019. Во-вторых, все недостающие библиотеки нашлись в vcpkg. Так что всё обновление свелось к монотонной конвертации проектов и выполнению vcpkg search и vcpkg install.

Ну а теперь немного предыстории.

Началось всё случайно. Как в «Магазинчике БО», история пишется дохлыми зайцами. В один прекрасный день пришёл ко мне коллега по проекту и спросил: «Ден, доколе?! Доколе ради простейших операций со строками мне придётся создавать копии, делать бессмысленные аллокации? Ведь давно уже есть string_view…». Так что, считай, благодаря «дохлому» string мы и решили, а не замахнуться ли нам… ну и замахнулись.

В тот момент проект сидел на Visual Studio 2010, и новые версии стандарта C++ с соответствующими печеньками госдепа нам только снились.

Так что мы хотели получить?

  1. Потенциальный буст производительности — без оценок, потому что не знали, к чему бы это привело, особенно в проекте, которому больше 10 лет;

  2. Возможность включать те сторонние библиотеки, на которые мы давно смотрели, но по тем или иным причинам не могли с ними работать (EASTL, Optick Profiler и т. д.);

  3. Сокращение времени разработки;

  4. Прокачать командные скилы. Применить на практике все возможности современного C++, т. к. оставаться на С++ 0x в 20-х годах XXI века — это профессиональная деградация.

А теперь — с чем столкнулись:

  1. Краши — сразу при запуске клиентского приложения из-под отладчика (delete(void*));

  2. Deadlocks — подтянулись спустя n недель. На наше счастье, он такой был один, и быстро удалось его отловить, а потом нагуглить, в чем причина (Thread-safe Local Static Initialization);

  3. Проблемы с позиционированием объектов в мире: террейн, например, улетел вникуда, а за ним мобы и все остальное (FPU control word);

  4. Магия с шаблонами и макросами;

  5. Исключительно кастомная ошибка: при запуске финальной сборки клиента игры мы проверяем раздел экспорта .exe-файла. Он должен быть пустым. Это требование связано с системой защиты проекта. Но в .export-секции оказалось очень много чего интересного, о чем я и расскажу в заключительной части (_CRT_STDIO_INLINE).

Но обо всем по порядку.

Краши (USER-DEFINED OPERATOR DELETE(VOID ))

  1. Краши происходили из-за нашего кастомного аллокатора памяти на основе dlmalloc: наверное, не секрет ни для кого, что игровые проекты не используют стандартные аллокаторы, которые не заточены под специфику игр.

  2. Стандартный аллокатор не блещет производительностью, что является краеугольным камнем игровых проектов. Кроме того, для игр очень страшна фрагментация памяти. Поскольку они очень “memory-consumption”, если адресное пространство сильно фрагментировано, то память быстро «заканчивается», и происходят креши. Наверное, для 64-битных игр это не критично, но для 32-битных, как у нас — весьма.

  3. В MinGW для проектов, которые собираются на C++ 14 и динамически линкуют стандартную библиотеку libstdc++, не вызывается переопределенный скалярный оператор delete(void ), если не переопределена его sized-версия (GCC Bugzilla – Bug 77726). 

  4. Оказалось, что в компиляторе Microsoft Visual C++ (по крайней мере, версии 14.21) есть похожая «особенность». Об этом баге добрые люди даже сообщали в Microsoft (Developer Community), но мы об этом узнали, уже справившись с проблемой. К чести товарищей из «Сиэтловской области», они эту «особенность» пофиксили.

Итого: переопределили sized-версию delete — и краши пропали. Вот так наступила перемога:

void __cdecl operator delete( void* ptr ) noexcept
{
a1_free( ptr );
}

void __cdecl operator delete( void* ptr, size_t /*sz*/ ) noexcept
{
a1_free( ptr );
}
void __cdecl operator delete[](void* ptr) noexcept
{
a1_free( ptr );
}

void __cdecl operator delete[]( void* ptr, size_t /*sz*/ ) noexcept
{
a1_free( ptr );
}

Здесь a1_free( ptr ) — наша собственная функция освобождения памяти.

Тут же хотелось бы отметить один интересный факт. Давайте посмотрим на разделы импорта одной из библиотек нашего проекта DataProvider.dll, занимающейся подтягиванием ресурсов игры (этот факт справедлив для всех библиотек проекта).

Раздел импорта библиотеки DataProvider.dll (Visual C++ 2010, Debug)
Раздел импорта библиотеки DataProvider.dll (Visual C++ 2010, Debug)
Раздел импорта библиотеки DataProvider.dll (Visual C++ 2010, Release)
Раздел импорта библиотеки DataProvider.dll (Visual C++ 2010, Release)
Раздел импорта библиотеки DataProvider.dll (Visual C++ 2019, Debug)
Раздел импорта библиотеки DataProvider.dll (Visual C++ 2019, Debug)
Раздел импорта библиотеки DataProvider.dll (Visual C++ 2019, Release)
Раздел импорта библиотеки DataProvider.dll (Visual C++ 2019, Release)

Как мы видим, в сборках Visual C++ 2010 и Visual C++2019 раздел импорта отличается. Debug-версии отличаются только наличием sized-версии operator delete в сборке Visual C++ 2019, а вот в Release-сборке версии 2019 вообще отсутствуют operator new/delete. Это отдельный интересный вопрос, который стоит задать компилятору и линковщику.

Далее нас ждали любимые дедлоки, связанные с инициализацией локальных статических переменных.

Thread-Safe Local Static Initialization

Существует интересный флажок компилятора /Zc:threadSafeInit, который сообщает ему, что инициализировать локальные статические переменные нужно в thread safe режиме. MSDN говорит, что поведение не определено, если происходит рекурсивная инициализация — то есть, поток выполняет функцию foo, содержащую объявление статической переменной var, и доходит до инициализации этой переменной. Переменная var представляет собой экземпляр очень хитро реализованного класса, конструктор которого может ещё раз вызвать функцию foo. По умолчанию флажок /Zc:threadSafeInit включен в Visual Studio 2015 и выше. Поэтому в нашем случае поведение оказалось вполне себе определено — бесконечный цикл ожидания инициализации переменной.

Вот небольшой кусочек кода, наглядно иллюстрирующий работу флажка /Zc:threadSafeInit. Помимо собственного глобального аллокатора мы используем  статические типизированные пулы для часто создаваемых/разрушаемых объектов. 

#define DEFINE_STATIC_POOL( ElementType, ElementsCount, PoolVarName ) \
static FORCEINLINE Mem::static_pool<ElementType, ElementsCount> &PoolVarName() \
{ \
static Mem::static_pool<ElementType, ElementsCount> __instance; \
return __instance; \
} \
class StaticPoolCreator##PoolVarName \
{ \
public: \
StaticPoolCreator##PoolVarName() \
{ \
PoolVarName(); \
} \
} static creator##PoolVarName;
}

Здесь мы объявляем статическую функцию со статической переменной instance внутри, стандартный синглтон Майерса. Затем объявляем класс, в конструкторе которого вызывается определенный нами синглтон, и объявляем экземпляр данного класса статическим. Такие телодвижения с известным ударным инструментом необходимы, чтобы ввести в заблуждение вероятного противника (как не в себя оптимизирующий компилятор) и не раздувать секцию .data результирующего .exe-файла клиента игры.

А так выглядит наш поток сознания в assembler:

7BC04944  call        __Init_thread_header (7BBC247Dh)  
7BC04949  add         esp,4  
7BC0494C  cmp         dword ptr ds:[7BD00944h],0FFFFFFFFh  
7BC04953  jne         event_sound3d_pool+59h (7BC04979h)  
7BC04955  mov         ecx,offset __instance (7BCDC4F8h)  
7BC0495A  call        Mem::static_pool<Sound::SoundEvent3D,448>::static_pool<Sound::SoundEvent3D,448> (7BBC1569h)  
7BC0495F  push        offset `event_sound3d_pool'::`2'::`dynamic atexit destructor for '__instance'' (7BC6C130h)  
7BC04964  call        _atexit (7BBC4DF4h)  
7BC04969  add         esp,4  
7BC0496C  push        7BD00944h  
7BC04971  call        __Init_thread_footer (7BBC3652h) 

Init_thread_header — функция Visual C++ Runtime, исходники которой можно посмотреть здесь: vcruntime/thread_safe_statics.cpp. Она начинает потокобезопасный блок инициализации статической переменной. Достигается это путем использования двух вспомогательных счетчиков и флажка, которые управляют этапами инициализации переменной.

Далее мы регистрируем деструктор нашей статической переменной и вызываем функцию Init_thread_footer. 

А здесь работает критическая секция, которая управляет доступом к описанным счетчикам и флажкам:

extern "C" void __cdecl _Init_thread_lock():

7BC63350  push        ebp  
7BC63351  mov         ebp,esp  
7BC63353  push        offset _Tss_mutex (7C051218h)  
7BC63358  call        dword ptr [__imp__EnterCriticalSection@4 (7C0521E0h)]  
7BC6335E  pop         ebp  
7BC6335F  ret  

После того, как мы разобрались с зависанием игрового клиента, мы столкнулись с ещё более интересной проблемой: отъехали координаты практически всех объектов в игровом мире. 

FPU Control Word

x87 FPU Control Word — это управляющее слово математического сопроцессора, с помощью которого можно управлять режимом округления, точностью, а также маскировать различные исключения. Существует определённый набор API для работы с этим control word — например, функция _controlfp_s. Она позволяет как прочитать, так и записать управляющее слово. 

Касаемо последней API — у нее есть сайд-эффекты, выражающиеся в том, что она, в зависимости от аппаратной платформы, затрагивает управляющий регистр SSE2( MXSCR- x86/x64), где также модифицирует флаги округления и не только.

К чему такая долгая предыстория? Дело в том, что, когда мы побороли краши и зависания и смогли на свежесобранной версии попасть в игровой мир, там нас ждал очень неприятный сюрприз. Странным образом отвалился террейн, все мобы и персонажи в мире упали в бездну, кто находился на каких-то объектах, участвовали в каком-то броуновском движении. Здания повисли в воздухе, а часть объектов взлетела в небо. К сожалению, в тот момент я не наделал скриншотов, чтобы поделиться этим глобальным апокалипсисом. Но, в целом, картина выглядела довольно удручающе. Всему виной оказалась одна из вендорских библиотек, которая меняла режим округления вещественных чисел: вместо округления к ближайшему целому отбрасывалась дробную часть.

Вкратце коснемся системы позиционирования «Аллодов». Она базируется на кубах размером 32x32x32 и смещениях внутри них. Игровая позиция задаётся парой векторов Position{ {  int, int, int }, { float, float, float } }, и любая позиция приводится к этому виду методом Position::Normalize(). Всё решение проблемы заключалось в модификации этого метода следующим образом:

FORCEINLINE void Position::Normalize()
{
. . . . . . . .

unsigned int prev_ctrl_word = 0;
_controlfp_s( &prev_ctrl_word, 0, 0 );
const unsigned int prev_round_mod = prev_ctrl_word & _MCW_RC;
_controlfp_s( &prev_ctrl_word, _RC_NEAR, _MCW_RC );

. . . . . . . .

_controlfp_s( &prev_ctrl_word, prev_round_mod, _MCW_RC );

. . . . . . . .
}

Метод Normalize предназначен для преобразования данных внутри Position в валидную позицию игрового мира. В начале метода мы сохраняем текущее значение управляющего слова и устанавливаем флаг, который меняет режим округления. Затем происходит преобразование позиции, после чего восстанавливается режим округления, который был до вызова нашей функции.

Почему так криво? Дело в том, что в отсутствие времени на выяснение поведения вендорской библиотеки в случае масштабного вмешательства в режим округления было принято решение пойти методом наименьшего сопротивления — «Чтобы ничего не поломать».

Шаблоны и макро-магия

Поборов, как казалось, основные трудности обновления, когда мы уже готовились замерять буст производительности, вылезла ещё одна проблема. В одном из кейсов общения с определенными мобами вылетел новый краш. Причина оказалась в коде, который был закомментирован в начале процесса обновления, чтобы просто собрать и запустить всё остальное.

Дело вот в чем. Мы объявляем массив структур с помощью запутанных макросов. Затем один из макросов генерит в каждой структуре указатель на функцию с типом lua_CFunction. Внутри каждой функции будет вызываться соответствующий объявлению метод класса Sound::ISound2D. Например, первый элемент массива генерирует структуру с функцией, которая будет вызывать метод bool Sound::ISound2D::Play( void ) и т.д.

DEFINE_LUA_STRONG_METHODS_START( Sound::ISound2D )
{ "Play", OBJECT_LUA_FUNCTION( bool, Sound::ISound2D, Play, void ) },
{ "Stop", OBJECT_LUA_FUNCTION( bool, Sound::ISound2D, Stop, bool ) },

Error C2440 'initializing': cannot convert from 'int (__cdecl *)(lua_State *)' to 'lua_CFunction'

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

Интересна ошибка тем, что проблемный тип функции определён где-то в недрах LUA-библиотеки следующим образом:

typedef int (lua_CFunction) (lua_State L);

Как мы видим, компилятор не может привести друг к другу два идентичных типа указателей на функции. __cdecl в объявлении типа lua_CFunction ни на что не влияет, т.к. дефолтным соглашением о вызовах в C++ как раз и является __cdecl. Так что типы совершенно идентичны.

Путем очень въедливого чтения сообщений компилятора стало понятно, что дело только в объявлениях вида:

{ "Play", OBJECT_LUA_FUNCTION( bool, Sound::ISound2D, Play, void ) },

В то время как объявление:

{ "Stop", OBJECT_LUA_FUNCTION( bool, Sound::ISound2D, Stop, bool ) },

— недовольств компилятора не вызывает. Разницы между строчками практически никакой, кроме типа последнего параметра метода класса. Однако, первый вариант упорно не нравится компилятору, а вот со вторым всё в порядке. Поэтому проблема решилась простой заменой во всех проблемных случаях void на bool. 

Из-за большого количества макросной магии и её не принципиальной важности разбирательства с ней сошли на нет. Но осталась задача на будущее — разобрать данное отличие поведения компиляторов MS VC 2010 и MS VC 2019.

_CRT_STDIO_INLINE

Как и любой проект, «Аллоды» имеют несколько конфигураций сборки, и вот на этапе сборки «финальной» конфигурации, которая и распространяется через Игровой центр MY.GAMES (назовём её FINALRELEASE), мы столкнулись с заключительной на сегодня проблемой.

Ниже приведен скриншот, функции на котором наверняка многим знакомы:

Как я уже говорил, при запуске финальной версии игры происходит проверка раздела экспорта на предмет его «девственной» чистоты. И скриншот показывает, что наша проверка не проходит. Но интересен не сам факт провала проверки, а функции, попавшие в .export-секцию, которые и фейлят проверку. Как нетрудно заметить, это API библиотеки stdio.

Давайте взглянем на то, как определяются данные API:

ucrt\corecrt_stdio_config.h:

#if defined _NO_CRT_STDIO_INLINE
    #undef _CRT_STDIO_INLINE
    #define _CRT_STDIO_INLINE
#elif !defined _CRT_STDIO_INLINE
    #define _CRT_STDIO_INLINE __inline
#endif

ucrt\stdio.h:

_Check_return_opt_
_CRT_STDIO_INLINE int __CRTDECL _fprintf_l(
    _Inout_                                 FILE*       const _Stream,
    _In_z_ _Printf_format_string_params_(0) char const* const _Format,
    _In_opt_                                _locale_t   const _Locale,
    ...)
#if defined _NO_CRT_STDIO_INLINE
;
#else
{
    int _Result;
    va_list _ArgList;
    __crt_va_start(_ArgList, _Locale);
    _Result = _vfprintf_l(_Stream, _Format, _Locale, _ArgList);
    __crt_va_end(_ArgList);
    return _Result;
}
#endif

Объявление выглядит довольно логичным и направленным на избавление от накладных расходов на функциональные вызовы. Но в нашем проекте такое объявление оказалось не предусмотрено.

Причина проблемы оказалась в библиотеке LuaJIT(a Just-In-Time Compiler for Lua). А вернее — в ее сборке. В проекте она собирается с помощью .bat-файла, в котором оказались интересные строки:

. . . . . . .
@setlocal
@set LJCOMPILE=cl /MD /nologo /c /O2 /W3 /D_CRT_SECURE_NO_DEPRECATE /D_CRT_STDIO_INLINE=__declspec(dllexport)__inline /I"include" /I"src" /Fo"obj/"
@set LJLINK=link /nologo
@set LJMT=mt /nologo
@set LJLIB=lib /nologo /nodefaultlib
@set DASMDIR=dynasm
@set DASM=%DASMDIR%\dynasm.lua
. . . . . . . 

Макрос D_CRT_STDIO_INLINE определяется образом, указанным выше, и затем передается компилятору. Таким образом, D_CRT_STDIO_INLINE определяется как __declspec(dllexport)__inline. С точки зрения компилятора, а заодно и здравого смысла, определение функции с префиксом __declspec(dllexport)__inline вполне законно, и на практике приводит к вполне логичным и ожидаемым результатам. Грубо говоря, компилятор заменяет вызовы функции внутри библиотеки, где она реализована, и где он может это сделать, на её тело. Но также он помещает её в раздел экспорта данной библиотеки.

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

Итоги

  1. Первое и самое главное — мы апдейтили проект до стандарта ISO C++ 17. И, наконец, получили возможность применять современные стандарты в разработке.

  2. Количество аллокаций в кадре снизилось в разы. В определённых моментах, например, с 600-700 до 200-250. Да, это всё-равно громадное значение, и далеко ещё не самое высокое. Но прогресс заметен.

  3. FPS увеличился в среднем на ~10-15% в «гладком» кадре. 

Что такое «гладкий» кадр? Будем считать, что это кадр, не нагруженный большим количеством интерфейсных событий. Например, даже этот относительно лёгкий замес порождает огромное число сообщений для апдейта интерфейса:

А вот так выглядит «гладкий» кадр:

4. Получили возможность дальнейшей оптимизации кодовой базы проекта на основании последних нововведений стандартов C++11/14/17.

5. Время сборки проекта и размер результирующего exe-файла клиента сколько либо заметно не изменились, что также является плюсом. 

Эпилог

Итак, основных поставленных целей мы достигли. Но были и приятные неожиданные бонусы. Разумеется, разработчики компиляторов не стоят на месте и стараются создавать всё более оптимизирующие версии, но даже с учётом этой тенденции один из примеров стал для меня откровением. 

Я решил посмотреть asm-код одного из тестов SSE-функций. Результат Visual C++ 2019 превзошел старую версию, как гроссмейстер школьника. При максимально идентичных настройках там, где Visual C++ 2010 просто заменил вызов inline-функции на соответствующую SSE-инструкцию, 2019-я версия увеличила шаг цикла и применила аналогичные требуемой AVX-инструкции. С учётом кода теста оптимизация выглядела вполне закономерной, но только старая версия не делала даже намёков на неё. 

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

И на самый-самый конец: а еще здесь можно ознакомиться со списком вакансий в нашей студии.

Комментарии (26)


  1. schulzr
    11.02.2022 14:52

    Добрый день, а у вас уже появился опыт использования EASTL?

    Действительно переход с stl дает заметный выигрыш в реальных сложных приложениях? Спасибо.


    1. dkolosov Автор
      11.02.2022 15:51
      +2

      Масштабно заменять nstl, на котором построен проект, на eastl нет смысла. Долго, очень багоопасно, профит, возможно, и будет, но есть сомнения, что грандиозный. Из того, что сравнивал, собственный nstl::hash_map при обработке нанонапильником оказался практически аналогичным eastl::hash_map, местами чуть быстрее. Правда пришлось масштабно перепилить итерирование, сильно отставало из-за кривой реализации.

      Если же говорить именно о std::stl, то у EA есть же много сравнений, там прирост местами очень значительный. Мы просто не используем std. Сейчас у нас гибрид из nstl и eastl. Что касается самого eastl, безусловно, очень гибкий инструмент в критичных к производительности проектах. Однозначно, must have.


      1. schulzr
        11.02.2022 18:03

        Спасибо!
        >> то у EA есть же много сравнений, там прирост местами очень значительный

        Но это их сравнения :) . Хотелось услышать вашу оценку.


        1. dkolosov Автор
          11.02.2022 18:23
          +3

          Как я уже сказал, в проекте мы std не используем. По сравнению с nstl удобнее в плане гибкости, в плане быстродействия преимуществ особых нет, только, если в nstl нет откровенного косяка, а они там встречаются. У меня есть планы провести отдельное небольшое сравнение нескольких реализаций stl с eastl, тогда смогу ответить более предметно. Но их результатам сравнений, имхо, можно доверять.


    1. beeruser
      12.02.2022 22:45

      EASTL нужен в первую очередь не для выигрыша в производительности, а для совместимости и предсказуемости.
      Когда вы собираете код на разных платформах/компиляторах/процессорных архитектурах, меньше всего вы хотите увидеть различное поведение std.

      Сейчас с этим проще, т.к. везде можно использовать Clang.


      1. dkolosov Автор
        13.02.2022 05:40
        +1

        Ну на самом деле по поводу приоритетов здесь не всё так однозначно. Для игр, например, попугаи имеют не менее определяющее значение, чем предсказуемость поведения "стандартных" шаблонов на разных платформах и архитектурах. Возможно, как раз производительность ставится во главу угла, а однозначность поведения идёт важным приятным бонусом.


  1. drakkonne
    11.02.2022 15:30

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


  1. drWhy
    11.02.2022 16:03
    +4

    Дорогу осилит идущий.

    Караван в игольном ушке


  1. rat1
    11.02.2022 17:56
    +9

    Я думал все игры умирают сразу после покупки их mail.ru)


    1. rat1
      11.02.2022 18:00
      +1

      Хотя, прочитав книгу "Нажми Reset. Как игровая индустрия рушит карьеры и дает второй шанс", Книга, Джейсон Шрейер, понял, что все большие компании имеют эту проблему)


      1. Suvitruf
        12.02.2022 12:59

        Да и не только большие. Можно почитать книгу Кушнера про Doom. Небольшие студии тоже этим страдают.


    1. Hait
      11.02.2022 18:54

      У АО был момент жесткой завязки на донаты. Где ты не имел high-level контента без денежек на счёте. Точно не помню когда это появилось - прямо перед покупкой mail или сразу после, но такой момент был.


  1. miha92
    11.02.2022 21:22

    Очень интересный материал! Спасибо!

    Успехов в дальнейшем развитии Аллодов!


  1. xFFFF
    12.02.2022 00:41
    +2

    В одной госкомпании РФ до сих пор пишут на Borland C++ Builder 6. Был очень удивлен, когда узнал. )


    1. jobber_man
      12.02.2022 03:02
      +2

      Если бы вы видели хотя бы одну из версий C++ Builder, выпущенных за 20 лет с момента релиза шестерки, то, возможно, не удивлялись бы :)

      А переезд на другой фреймворк это всегда дорого (иногда, очень дорого, если у вас миллионы строк кода) и далеко не всегда оправданно.


    1. eurol
      14.02.2022 08:54

      Почему-то вспомнился анекдот про мужика, которому жена звонит, чтобы сказать, что по радио передали, что один водитель едет против движения… :)


  1. VXP
    12.02.2022 02:03
    +2

    2010 -> 2019? Уверен, с VC++ 6.0 на 2019 помуторнее будет)

    Репо с примером, но закрыто, доступ по реквестам


  1. gvtret
    12.02.2022 12:37
    +3

    Как мы видим, в сборках Visual C++ 2010 и Visual C++2019 раздел импорта отличается. Debug-версии отличаются только наличием sized-версии operator delete в сборке Visual C++ 2019, а вот в Release-сборке версии 2019 вообще отсутствуют operator new/delete. Это отдельный интересный вопрос, который стоит задать компилятору и линковщику.

    А какой тут вопрос? Вызов стандартный. Исключения отключены. Вызов только одной функции с параметром без преобразований. Компилятор честно считает, что не зачем городить обертку над функцией, если никаких выделений и преобразований нет. Все просто. Компилятор прав. А линковшику пофиг))


    1. dkolosov Автор
      13.02.2022 05:42

      Ну компилятор VS2010 находится в тех же условиях, однако, он не увидел, что от обёртки можно избавиться.


      1. IGR2014
        13.02.2022 12:47

        Ну тогда это не проблема VS2019, это проблема VS2010.

        Современный C++ очень сильно полагается на оптимизатор


  1. Xadok
    12.02.2022 16:10

    В статье упоминается как msvc, так и mingw. Так чем собираете в итоге? Cmake осилили или все-таки нет?


    1. dkolosov Автор
      13.02.2022 05:45

      Ответ на первый вопрос, очевидно, следует из названия статьи. Ну а второй отпадает, ввиду ответа на первый.


      1. IGR2014
        13.02.2022 12:46

        Почем второй отпадает? У меня CMake отлично переваривает генерацию сборки под MSVC/ClangCL. Без никаких проблем


  1. oleg-m1973
    12.02.2022 20:05
    +1

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


  1. IvanIvanc
    13.02.2022 01:37

    Почему клиентское приложение аллодов показывает красное качество работы клиентского приложения при этом не утилизируя полностью видеокарту и процессор?


    1. dkolosov Автор
      13.02.2022 05:44
      +1

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