Пролог: Для начала я расскажу о проекте, чтобы были представления о том как мы работали над проектом и для воссоздания той боли, которую мы чувствовали.
Я как разработчик вступил в проект в 2015-2016 точно не помню, но он работал 2-3 года ранее. Проект был очень популярен в своей сфере, а именно игровых серверов. Как странно не звучало, но проекты по игровым серверам ведутся и по сей день, недавно вакансии видел и чуток поработал в одной команде. Поскольку игровые сервера строятся на уже созданной игре, следовательно для разработки используется скриптовый язык который встроен в движок игры.
Мы разрабатываем почти с нуля проект на Garry’s Mod (Gmod), важно подметить, что на момент написания статьи Гарри создает уже новый проект S&Box на движке Unreal Engine. Мы же до сих пор сидим на Source.

«Чем страшна ваша история?» — спросите вы.
У нас сильная тематика игрового сервера, а именно «Сталкер» и еще с элементами ролевых игр (РП), сразу встает вопрос — «А как все реализовать это на одном сервере?».
Учитывая что Source движок старый (2013 версии используется в Gmod еще и 32 битный), большие карты не сделаешь, малые ограничения на количество Entity, Mesh и много чего.
Первым делом начальное написание было сложным (многие действия из разряда: выкинуть предмет, поднять предмет писались с нуля), в надежде думая что дальше будет легче, но требования росли. Механика игры была готова, оставалось сделать интеллект, агрейд и всякие штучки. Вообщем все переносили как могли.

Проблемы начались уже во время работы релизной первой версии, а именно (лаги, задержки сервера).
Вроде мощный сервер мог спокойно обрабатывать запросы и держать весь Gamemode.
Лаги были как на серверной стороне из-за большого количества игроков, так как один игрок съедает много оперативной памяти порядка 80-120 мб (не считая еще предметов в инвентаре, навыков и т.д.), так и на клиентской стороне было сильное понижение фпс.
Мощности ЦП не хватало на обработку физики, приходилось меньше использовать объекты имеющие физические свойства.
Так еще вдобавок были наши самописные скрипты, которые вообще никак не были оптимизированы.

Во первых мы конечно же почитали статьи по оптимизации в Lua. Даже доходило досуицидатого что хотели писать DLL на C++, но проблема возникла в скачивании DLL с сервера клиентами. С помощью C++ для DLL можно написать спокойно перехватывающий данные программу, разработчики Gmod добавили расширение в исключения для скачивания клиентами (безопасность, хотя на самом деле никогда ее и не было). Хотя было бы удобно и Gmod стал бы гибче, но опаснее.
Далее мы смотрели в профайлер (благо умные люди написали его) и там творился ужас в функциях, было замечено, что уже изначально в движковой библиотеке Gmod есть очень медленные функции.
Если вы пытались писать в Gmod, то вы прекрасно знаете что есть библиотека встроенная под названием math.
И самые медленные функции в ней конечно же math.Clamp и math.Round.
Покопавшись в коде людей было замечено что функциями кидались в разные стороны, почти везде она применяется, но неправильно!
Давайте уже к практике. К примеру мы хотим округлить координаты вектора позиции для перемещения энтити (к примеру игрока).
3 сложных функции округления, но ничего серьезного, если конечно не в цикле и не часто используемый, а вот Clamp еще тяжелее.
Следущий код часто используется в проектах и никто ничего не хочет менять.
К примеру self указывает на объект игрока и у него есть придуманная нами локальная переменная, которая при перезаходе на сервер обнуляется, math.Clamp по сути как цикл, делает плавное присвоение, любят плавный интерфейс делать на Clamp.
Проблемы возникают когда это работает на каждом игроке, который заходит на сервер. Редкий случай, но если на сервер зайдет сразу 5-15 (зависит от конфигурации сервера) в один момент времени и у всех начнет работать эта маленькая и простая функция, то на сервере будут хорошие задержки ЦП. Все еще хуже если math.Clamp в цикле.
Оптимизация на самом деле очень простая, сильно нагружающие функции локализируете. Вроде примитив, но в 3 gamemode и многих аддонах видел этот медленный код.
Если вам надо получить значение и использовать его в будущем, не надо его еще раз получать если оно не изменяется. Ведь игрок заходя на сервер в любом случае получит голод равный значению 100, поэтому этот код в разы быстрее.
Все хорошо, стали смотреть дальше, что да как устроено. В итоге мы завелись манией все оптимизировать.
Мы заметили что стандартный цикл for медленный и мы решили придумать свой велосипед который будет быстрее (про блэкджек не забыли) и тут началась самая дичь.

Судя по затратам времени на наш цикл и использование его в коде, мы зря пытались это делать поскольку он нашел применение только в спавне на карте аномалий после выброса и очистки их.
И так к коду. К примеру надо найти все энтити с названием в начале anom, такое имя класса имеют у нас аномалии.
Вот for нормального скриптера на Lua Gmod:
Вот for курильщика:
Сразу видно что такойг*внокод будет медленнее стандартного «for in pairs», но как оказалось нет.
Для полного анализа этих вариантов циклов их надо перевести в обычный Lua скрипт.
К примеру anomtable будет иметь 5 элементов.
Удаление заменим обычным сложением. Главное посмотреть разницу в количестве инструкций между двумя вариантами реализации цикла for.
Ванильный цикл:
Наш велик:
Давайте же посмотрим на интерпретаторский код (подобие ассемблера, высокоуровневым программистом смотреть под спойлер не рекомендуется).
На всякий случай уберите джунов от экранов. Я предупреждал.
Неопытный просто взглянув скажет что обычный цикл быстрее поскольку инструкций меньше (15 против 19).
Но надо не забывать что каждая инструкция в интерпретаторе имеет такты процессора.
Судя по дизассемблерному коду в первом цикле есть инструкция forloop заранее написанная для работы с массивом, массив загружается в память становится глобальным, прыгаем по элементам и прибавляем константу.
Во втором варианте способ иначе, который больше основывается на память, он получает таблицу изменяет элемент, устанавливает таблицу, проверяет на наличие nil и опять вызывает.
Второй наш цикл быстрый по причине того что в одной инструкции слишком много условий и действий ( R4,R5 := R1(R2,R3); if R4 ~= nil then begin PC = 12; R3 := R4 end ) из-за этого она очень многожрет употребляет кушает тактов ЦП на выполнение, прошлый опять же больше завязан на памяти.
Инструкция forloop при большом количестве элементов сдается, нашему циклу по скорости прохода всех элементов. Связано это что обращение напрямую по адресу быстрее, меньше всяких плюшек от pairs. (А еще у нас нет отрицания)
Этот долгострой-проект еще увидел вторую версию себя где оптимизации очень много в коде, но про другие оптимизации я расскажу в следующих статьях. Поддержите критикой или комментарием, поправьте если ошибаюсь.
Я как разработчик вступил в проект в 2015-2016 точно не помню, но он работал 2-3 года ранее. Проект был очень популярен в своей сфере, а именно игровых серверов. Как странно не звучало, но проекты по игровым серверам ведутся и по сей день, недавно вакансии видел и чуток поработал в одной команде. Поскольку игровые сервера строятся на уже созданной игре, следовательно для разработки используется скриптовый язык который встроен в движок игры.
Мы разрабатываем почти с нуля проект на Garry’s Mod (Gmod), важно подметить, что на момент написания статьи Гарри создает уже новый проект S&Box на движке Unreal Engine. Мы же до сих пор сидим на Source.
Который вообще не подходит для нашей тематики сервера.

«Чем страшна ваша история?» — спросите вы.
У нас сильная тематика игрового сервера, а именно «Сталкер» и еще с элементами ролевых игр (РП), сразу встает вопрос — «А как все реализовать это на одном сервере?».
Учитывая что Source движок старый (2013 версии используется в Gmod еще и 32 битный), большие карты не сделаешь, малые ограничения на количество Entity, Mesh и много чего.
Кто работал на движке, поймет.Получается, задача вообще невыполнима, сделать чистый мультиплеерный сталкер с квестами, RPG-элементами из самого оригинала и желательно малый сюжет.
Первым делом начальное написание было сложным (многие действия из разряда: выкинуть предмет, поднять предмет писались с нуля), в надежде думая что дальше будет легче, но требования росли. Механика игры была готова, оставалось сделать интеллект, агрейд и всякие штучки. Вообщем все переносили как могли.

Проблемы начались уже во время работы релизной первой версии, а именно (лаги, задержки сервера).
Вроде мощный сервер мог спокойно обрабатывать запросы и держать весь Gamemode.
Простое описание gamemode
Так называется комплекс скриптов написанный для описания механики самого сервера
Например: хотим тематику нынче популярных «Королевских битв», значит и название соответствовать должно и механика игры тоже. «Спавн игроков в самолете, можно подбирать вещи, игроки могут общаться, нельзя надевать на себя больше 1 шлема и т.д.» — все это описывается механикой игры на сервере.
Например: хотим тематику нынче популярных «Королевских битв», значит и название соответствовать должно и механика игры тоже. «Спавн игроков в самолете, можно подбирать вещи, игроки могут общаться, нельзя надевать на себя больше 1 шлема и т.д.» — все это описывается механикой игры на сервере.
Лаги были как на серверной стороне из-за большого количества игроков, так как один игрок съедает много оперативной памяти порядка 80-120 мб (не считая еще предметов в инвентаре, навыков и т.д.), так и на клиентской стороне было сильное понижение фпс.
Мощности ЦП не хватало на обработку физики, приходилось меньше использовать объекты имеющие физические свойства.
Так еще вдобавок были наши самописные скрипты, которые вообще никак не были оптимизированы.

Во первых мы конечно же почитали статьи по оптимизации в Lua. Даже доходило до
Далее мы смотрели в профайлер (благо умные люди написали его) и там творился ужас в функциях, было замечено, что уже изначально в движковой библиотеке Gmod есть очень медленные функции.
Если вы пытались писать в Gmod, то вы прекрасно знаете что есть библиотека встроенная под названием math.
И самые медленные функции в ней конечно же math.Clamp и math.Round.
Покопавшись в коде людей было замечено что функциями кидались в разные стороны, почти везде она применяется, но неправильно!
Давайте уже к практике. К примеру мы хотим округлить координаты вектора позиции для перемещения энтити (к примеру игрока).
local x = 12.5
local y = 14.9122133
local z = 12.111
LocalPlayer():SetPos( Vector( Math.Round(x), Math.Round(y), Math.Round(z) )
3 сложных функции округления, но ничего серьезного, если конечно не в цикле и не часто используемый, а вот Clamp еще тяжелее.
Следущий код часто используется в проектах и никто ничего не хочет менять.
self:setLocalVar("hunger", math.Clamp(current + 1, 0, 100))
К примеру self указывает на объект игрока и у него есть придуманная нами локальная переменная, которая при перезаходе на сервер обнуляется, math.Clamp по сути как цикл, делает плавное присвоение, любят плавный интерфейс делать на Clamp.
Проблемы возникают когда это работает на каждом игроке, который заходит на сервер. Редкий случай, но если на сервер зайдет сразу 5-15 (зависит от конфигурации сервера) в один момент времени и у всех начнет работать эта маленькая и простая функция, то на сервере будут хорошие задержки ЦП. Все еще хуже если math.Clamp в цикле.
Оптимизация на самом деле очень простая, сильно нагружающие функции локализируете. Вроде примитив, но в 3 gamemode и многих аддонах видел этот медленный код.
Если вам надо получить значение и использовать его в будущем, не надо его еще раз получать если оно не изменяется. Ведь игрок заходя на сервер в любом случае получит голод равный значению 100, поэтому этот код в разы быстрее.
local value = math.Clamp(current + 1, 0, 100)
self:setLocalVar("hunger", value)
Все хорошо, стали смотреть дальше, что да как устроено. В итоге мы завелись манией все оптимизировать.
Мы заметили что стандартный цикл for медленный и мы решили придумать свой велосипед который будет быстрее (про блэкджек не забыли) и тут началась самая дичь.

СПОЙЛЕР
У нас даже получилось сделать быстрый самый цикл на Lua Gmod, но при условии что элементов должно быть больше 100.
Судя по затратам времени на наш цикл и использование его в коде, мы зря пытались это делать поскольку он нашел применение только в спавне на карте аномалий после выброса и очистки их.
И так к коду. К примеру надо найти все энтити с названием в начале anom, такое имя класса имеют у нас аномалии.
Вот for нормального скриптера на Lua Gmod:
local anomtable = ents.FindByClass("anom_*")
for k, v in pairs(anomtable) do
v:Remove()
end
Вот for курильщика:
Сразу видно что такой
local b, key = ents.FindByClass("anom_*"), nil
repeat
key = next(b, key)
b[key]:Remove()
until key != nil
Для полного анализа этих вариантов циклов их надо перевести в обычный Lua скрипт.
К примеру anomtable будет иметь 5 элементов.
Удаление заменим обычным сложением. Главное посмотреть разницу в количестве инструкций между двумя вариантами реализации цикла for.
Ванильный цикл:
local anomtable = { 1, 2, 3, 4, 5 }
for k, v in pairs(anomtable) do
v = v + 1
end
Наш велик:
local b, key = { 1, 2, 3, 4, 5 }, nil
repeat
key = next(b, key)
b[key] = b[key] + 1
until key ~= nil
Давайте же посмотрим на интерпретаторский код (подобие ассемблера, высокоуровневым программистом смотреть под спойлер не рекомендуется).
На всякий случай уберите джунов от экранов. Я предупреждал.
Дизассемблер ванильного цикла
; Name: for1.lua
; Defined at line: 0
; #Upvalues: 0
; #Parameters: 0
; Is_vararg: 2
; Max Stack Size: 7
1 [-]: NEWTABLE R0 5 0 ; R0 := {}
2 [-]: LOADK R1 K0 ; R1 := 1
3 [-]: LOADK R2 K1 ; R2 := 2
4 [-]: LOADK R3 K2 ; R3 := 3
5 [-]: LOADK R4 K3 ; R4 := 4
6 [-]: LOADK R5 K4 ; R5 := 5
7 [-]: SETLIST R0 5 1 ; R0[(1-1)*FPF+i] := R(0+i), 1 <= i <= 5
8 [-]: GETGLOBAL R1 K5 ; R1 := pairs
9 [-]: MOVE R2 R0 ; R2 := R0
10 [-]: CALL R1 2 4 ; R1,R2,R3 := R1(R2)
11 [-]: JMP 13 ; PC := 13
12 [-]: ADD R5 R5 K0 ; R5 := R5 + 1
13 [-]: TFORLOOP R1 2 ; R4,R5 := R1(R2,R3); if R4 ~= nil then begin PC = 12; R3 := R4 end
14 [-]: JMP 12 ; PC := 12
15 [-]: RETURN R0 1 ; return
Дизассемблер велосипедного цикла
; Name: for2.lua
; Defined at line: 0
; #Upvalues: 0
; #Parameters: 0
; Is_vararg: 2
; Max Stack Size: 6
1 [-]: NEWTABLE R0 5 0 ; R0 := {}
2 [-]: LOADK R1 K0 ; R1 := 1
3 [-]: LOADK R2 K1 ; R2 := 2
4 [-]: LOADK R3 K2 ; R3 := 3
5 [-]: LOADK R4 K3 ; R4 := 4
6 [-]: LOADK R5 K4 ; R5 := 5
7 [-]: SETLIST R0 5 1 ; R0[(1-1)*FPF+i] := R(0+i), 1 <= i <= 5
8 [-]: LOADNIL R1 R1 ; R1 := nil
9 [-]: GETGLOBAL R2 K5 ; R2 := next
10 [-]: MOVE R3 R0 ; R3 := R0
11 [-]: MOVE R4 R1 ; R4 := R1
12 [-]: CALL R2 3 2 ; R2 := R2(R3,R4)
13 [-]: MOVE R1 R2 ; R1 := R2
14 [-]: GETTABLE R2 R0 R1 ; R2 := R0[R1]
15 [-]: ADD R2 R2 K0 ; R2 := R2 + 1
16 [-]: SETTABLE R0 R1 R2 ; R0[R1] := R2
17 [-]: EQ 1 R1 K6 ; if R1 == nil then PC := 9
18 [-]: JMP 9 ; PC := 9
19 [-]: RETURN R0 1 ; return
Неопытный просто взглянув скажет что обычный цикл быстрее поскольку инструкций меньше (15 против 19).
Но надо не забывать что каждая инструкция в интерпретаторе имеет такты процессора.
Судя по дизассемблерному коду в первом цикле есть инструкция forloop заранее написанная для работы с массивом, массив загружается в память становится глобальным, прыгаем по элементам и прибавляем константу.
Во втором варианте способ иначе, который больше основывается на память, он получает таблицу изменяет элемент, устанавливает таблицу, проверяет на наличие nil и опять вызывает.
Второй наш цикл быстрый по причине того что в одной инструкции слишком много условий и действий ( R4,R5 := R1(R2,R3); if R4 ~= nil then begin PC = 12; R3 := R4 end ) из-за этого она очень много
Инструкция forloop при большом количестве элементов сдается, нашему циклу по скорости прохода всех элементов. Связано это что обращение напрямую по адресу быстрее, меньше всяких плюшек от pairs. (А еще у нас нет отрицания)
Вообще по секрету, любое использование отрицания в коде замедляет его, это уже проверено тестами и временем. Отрицательная логика будет медленнее работать поскольку в ALU процессора есть отдельный вычислительный блок «инвертор», для работы унарного операнда (not, !) надо обращаться к инвертору и это займет дополнительное время.Вывод: Все стандартное не всегда лучше, свои велосипеды могут принести пользу, но опять же на реальном проекте не стоит придумывать их, если вам важна скорость выхода в релиз. У нас в итоге полная разработка идет с 2014 и по сей день, этакий еще один «ждалкер». Хотя вроде обычный игровой сервер который ставится за 1 день и настраивается полностью под игру за 2 дня, но вносить что-то новое это надо уметь.
Этот долгострой-проект еще увидел вторую версию себя где оптимизации очень много в коде, но про другие оптимизации я расскажу в следующих статьях. Поддержите критикой или комментарием, поправьте если ошибаюсь.