1 may
2022

Jass MythBusters

Added by , published
» Раздел: Основы

Вступление

Для чего эта статья?

За относительно долгое время, что я провёл работая с Jass, я как и другие пользователи получил уйму информации из уст в уста о тех или иных функциях, методах или же проблемах, которые присутствуют в языке Jass. Однако, практически большая часть из этой информации не имела ни подтверждения, ни малейшего примера, фактически всё было основано на "наблюдении" и якобы "тестах". Потому, когда я дополнил начальную наработку MemHack, и добавил возможность делать бенчмарки, а также разобрал структуры игры и заручился CheatEngine для разбора байткода Jass, мне стало интересно проверить эти мифы на деле.
Потому - эта статья будет служить как фактическим доказательством или опровержения того или иного мифа исходя из реальных данных, основанных на реальных тестах, которые можно будет воспроизвести и убедиться в их действительности. Конечно же эта статья уже опоздала в плане своей полезности, но это не отменяет того, что для всё ещё заинтересованных личностей она будет очень и очень полезной.

Но всё же.. зачем?

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

Список Мифов

Локальные хендлы всегда утекают!

» раскрыть
Ранее считалось, что любая локальная сложная переменная (всё, что является handle, то бишь юниты, способности и так далее) утекают и затрачивают 4 байта памяти вне зависимости обнулена она или нет. Однако - это оказалось лишь на половину правдой, ибо утечка ЕСТЬ, однако лишь в случае, если мы СОЗДАЁМ ОБЪЕКТ и присваиваем её в локальную переменную и в конце кода мы не обнуляем как раз эту переменную. То бишь мы можем передать референс этой локальной переменной в глобальную, что позволит нам обнулить локальную переменную и фактически убрать утечку связанную со "сложными" локальными переменными.
Пример:
Это ВЫЗОВЕТ утечку.
	function DoSomething takes nothing returns nothing
		local unit u = CreateUnit( Player( 0 ), 'hpea', 0., 0., 270. )
		....
	endfunction
Однако, если же вы вызовете RemoveUnit( u ), то это не будет являться утечкой, так как юнит был обработан и удалён.
Это НЕ ВЫЗОВЕТ утечку
	function DoSomething takes nothing returns nothing
		local unit u = CreateUnit( Player( 0 ), 'hpea', 0., 0., 270. )
		....
		set u = null
	endfunction
Это НЕ ВЫЗОВЕТ утечку
	globals
		unit uTemp = null
	endglobals
	
	function main takes nothing returns nothing
		set uTemp = CreateUnit( Player( 0 ), 'hpea', 0., 0., 270. )
	endfunction

	function DoSomething takes nothing returns nothing
		local unit u = uTemp
		....
	endfunction
Чтобы понять, почему же так, стоит взглянуть на байткод Jass, взглянем на вариант без и с утечкой.
Для начала, предоставлю скриншот того, как же я получаю JASM байткод, я написал функцию получения Джасс ноды из игры по её имени, то бишь это кусок из хештаблицы, в ней же хранится ссылка на байткод, что она выполняет по оффсету 0x18.
теперь рассмотрим один из вариантов:
Мы получаем байткод:
» байткод
05070000
00000F80
0E290700
00000F7D
11290000
00000F80
0C000000
00000000
27000000
Что выдаёт нам:
» Результат
0x05 = local | 0x07 = type где номер = тип, 0x07 = Handle | 0x00 = nothing | 0x00 = nothing
argument = variable id ( то бишь 0xF80 ) переменные в Jass индексируются и далее вызываются по индексу.
0x0E = getvar | 0x29 = register (номер регистра) | 0x07 = type | 0x00
argument = variable id ( то бишь 0xF7D )
0x11 = setvar | 0x29 = register | 0x00 | 0x00
argument = variable id ( то бишь 0xF80 )
0x0C = literal | 0x00 | 0x00 | 0x00 - ибо функция возвращает nothing, потому в R0 записывается 0.
0x27 = return
Если же Вам всё это ничего не говорит (большинству скорее всего это ничего и не скажет), это банально значит, что локальные переменные фактически равным аргументам функций (которые не нужно обнулять). Простой ответ - сами переменные не утекают ни при каких обстоятельствах и они преобразуются в нечто статичное, и им выделена память единожды. Потому, утечки вызываются фактически банальной проблемой логики в самом коде, то бишь вы создали хендл, а затем его не удалили и так может повторяться уйму раз. Но это не утечка, ибо тогда по этой логике call CreateUnit и так далее - утечки, ибо этот юнит просто создан и дальше нам нужно его найти, чтобы убить или удалить и так далее.
Подводя итоги, можно смело сказать, что никакого "ужаса" от использования локалок попросту нету, конечно же это не отменяет факта, что даже их стоит использовать с умом и их обнуление просто на просто необходимо, чтобы занятые регистры были очищены когда функция закончится и будет выполнена 0x27 (return) что повлечёт очистку стека. Да и фактически нет никакого оправдания тем, кто не обнуляет локальные "сложные" переменные, ибо их обнуление не добавляет нагрузки на JassVM и фактически уменьшает возможные утечки.
Хотелось бы внести поправку/дополнение (за что спасибо PTR153), в случае если вы создаёте объект и присваиваете его локалке, или же создаёте какой-либо объект внутри функции и присваиваете его локальной переменной, то её ОБЯЗАТЕЛЬНО нужно обнулить, в случае если созданный объект не был удалён и вы продолжаете его использовать, то всё что нужно - это присвоить его какой-нибудь глобальной переменной и вернуть её.
» Краткий пример
function TestFunctionEx takes nothing returns nothing
    local location loc = Location( .0, .0 )
    set Loc = loc
    set loc = null
    call RemoveLocation( Loc )
endfunction
То бишь логика в том, что мы можем подать объект из локальной переменной в глобальную и далее уже без утечки провести с ней операцию. Однако есть исключения - группа, группа почему-то всегда и стабильно вызывает утечку в 1 байт, даже со схожим принципом.

Нативка X быстрее нативки Y!

» раскрыть
Я думаю многие частенько встречались с обсуждениями скоростей нативок, и чаще всего под "удар" попадала GetUnitX против GetWidgetX, и общепринятно, что GetWidgetX быстрее чем GetUnitX. Однако, так ли это? Так давай же узнаем!
Для начала, я предложу Вам посмотреть псевдокод game.dll обоих функций:
Как видите, он фактически идентичен, CUnit является расширением CWidget, однако обработка позиции делается всегда на уровне CWidget, а так как это расширенный класс, то разницы в скорости не может быть физически. Собственно потому и получается вот так:
» Сравнение скорости GetUnitX и GetWidgetX
Обе функции заняли 5 мс на 10000 повторов, значит реальная задержка одного повтора 500 наносекунд +- 1-2 наносекунды погрешности. Конечно гигантский плюс GetWidgetX в её универсальности, ибо её можно применить ко всем видам виджетов без типизации, однако на этом её плюсы заканчиваются и в остальном она идентична GetUnitX.

Хештаблица медленная!

» раскрыть
Это пожалуй один из самых ужасных мифов, которые просто на просто вводят меня в бешенство. Конечно же, этот миф "двоякий", ибо вопрос заключается в том, что в понятии "медленная" и в сравнении с чем? Зачастую те, кто используют vJass структуры, сравнивают хещтаблицу с этими псевдо-структурами, опираясь на то, что обращение к массиву - всегда быстрее чем обращение к хештаблице. Перед тем, как опровергнуть этот миф, я обязан согласиться с тем, что обращение к массиву конечно же быстрее, ибо JASM байткод вызывает 0x10 (getvar[]) и фактически берёт из уже "вшитого" списка функций по индексу, то бишь то, что делает хештаблица, только на более "низком" уровне.
Чтобы провести анализ скорости, давайте возьмём обычный массив чисел и число из хештаблицы и посмотрим эти варианты. Для простоты тестов, я не буду использовать StringHash для создания ключей, скорость с ними будет показан позже, и пример "ускорения" хештаблицы для особых фанатиков псевдоскорости.
» Сравнение скорости сохранения значения
» Сравнение скорости загрузки значения
Как мы видим getvar[] и setvar[] у массивов равен по скорости, точно так же как и Save равен по скорости Load у хештаблиц, потому далее будут сравниваться именно скорости загрузок (ибо мы чаще загружаем, чем сохраняем данные).
Смею предположить, что просмотрев примеры, будет вопрос "но тут же сравниваются нулевые ключи с последним индексом массива, так нечестно!", а что если я Вам скажу, что разница в значении числа создаёт минимальную погрешность и не более? Не верится? Так давай те же проверим!
» "Честное" сравнение скорости загрузки значения
Как видите, скорость не изменилась, однако, если мы теперь рассмотрим варианты со StringHash, то разница будет более заметной. Рассмотрим в начале самый распространённый вариант применения (который в целом я использую у себя в Jass коде).
» Сравнение скорости загрузки значение через StringHash
Ох как! Разница почти в 4.5 раз. Однако, не забывайте, что фактическая разница исчисляется на 1 вызов и не берёт во внимание таймер перебора всех значений в структуре, то бишь это сравнение строго одного массива и значения в хеше. Рассмотрим их фактические задержки, 3 мс / 10000 = 300 наносекунд и 13 мс / 10000 = 1300 наносекунд, то бишь борьба идёт за 1000 наносекунд, что является 0.001 мс или же 0.000001 секунды. Потому мне лично сложно представить, где эта разница даст о себе знать и как часто делается 10000 повторов или же вызовов чего-либо.
А теперь рассмотрим вариант "ускорения" хештаблицы, я думаю Вы прекрасно догадываетесь как же это можно сделать, но всё же рассмотрим это!
» Ускорение хештаблицы
Как видите, разница в скорости упала вновь до +- прежнего значения с погрешность, что в итоге делает разницу вновь лишь в 2 раза, однако даже так - эта разница слишком минимальна, чтобы это вызывало каке-либо проблемы. Для сравнения, средняя функция мемхака порой в 20 раз медленнее нативок, но это не делает эти функции абсолютно непригодными для использования, ведь так?

Хештаблица занимает слишком много памяти игры!

» раскрыть
Спасибо JackFastGame за напоминание об этом мифе. Для того, чтобы была возможность сравнивать или же оценивать занимает ли тот или иной объект много места, нужно найти его структуру и посмотреть, как игра работает с ними. Потому в начале стоит рассмотреть их фактические изначальные размеры, которые выделяет игра.
» Размер менеджера хештаблиц
» Размер хештаблицы
Для прояснения, игра создаёт менеджер Хештаблиц единожды, каждая хештаблица (всего их может быть 256) фактически - это дополнение внутриигровой хештаблицы (ибо не стоит забывать, что абсолютно все объекты в игре загружаются игрой из хештаблицы. Это в целом Вы можете увидеть достаточно подробно в МемХаке, где достаточно часто считываются объекты через их хешключи.
Пример такого в игре:
Но что же тогда делает хештаблица, которую мы можем создать в Jass? Она является ничем иным, как "дочерней" таблицей главной таблицы, которой выделено 0-255 ключей, то бишь 256 индексов, что можно увидеть тут:
» Максимальной количество Хештаблиц
Собственно, если хештаблица не имеет никаких вписанных в неё значений, то её "изначальный" вес будет равен 0x28 байтам, далее каждый новый "хешключ" (который состоит из родительского и дочернего ключа) будет добавлять по 4 байта (то бишь никакого отличия от массивов). Исключение - это "стринг", там каждая буква = один байт, что опять же идентично обычным переменным.
Подводя итоги, можно смело сказать, что хештаблица не представляет собой какую-то громоздкую систему и её "засорение" всегда будет на руках её пользователя, однако хештаблица нам позволяет динамически очищать память, путём RemoveSavedHandle и прочего или же полной очистки дочерних ключей или же всей хештаблицы, что освободит всю занятую ей память.

Deg2Rad и Rad2Deg медленные!

» раскрыть
Это как вы скорее всего и догадались очередной миф, который базируется вокруг "идеи", по которой прямое умножение джасс переменных должно быть быстрее нативок, однако это не так. Давайте начнём с примеров:
Давайте теперь рассмотрим Rad2Deg:
Результат одинаковый в обоих случаях, но почему же? Для этого нам нужно посмотреть в обработчик JASM байткода, который ссылается на опкод 0x22 (multiplication):
» Обработкич Байткода
А теперь посмотрим на Deg2Rad:
В итоге получается, что умножение фактически превосходит вызов нативки по общим операциям, но всё это сводится к погрешности, которая банально делает эти операции равными. Такая же учесть следует и за Pow( dx, 2 ) в сравнении с dx * dx, они тоже равны по скорости.

Юниты жрут больше фпса чем Эффекты!

» раскрыть
С выходом Warcraft 3 Reforged и с появлением API для эффектов, начал зарождаться вопрос, а что же в итоге эффективнее и меньше нагружает игру? Отвечу кратко, эффекты конечно же нагружают меньше, ввиду того, что эффект - это расширение CSpriteUber, а CUnit расширяет CWidget, который хранит не только CSpriteUber, а так же круги выделения, полосу здоровья, и прочую информацию. Говоря проще разница структур фактически колоссальная.
Однако, если вы думаете, что итог очевиден, то вы ошибаетесь. На деле игре фактически до шляпы, ибо без наведения мышки на юнита/выделения и если полосы здоровья скрыты, то нужная мощь GPU используется одинаково, как доказательство, как обычно предъявляю скриншоты.
Подводя итоги, можно смело сказать, что фактически гигантской или же ощутимой разницы просто нет и мне не понятно откуда вообще взялся миф об этом. Но надеюсь это более чем опровергает этот миф.

Операция not true медленнее чем true == false (или схожие аналоги)

» раскрыть
Данный миф существует с давних-давних времён, а если ещё точнее образовался с hiveworkshop, когда некто пытался замерять задержку исполнения функций (сама библиотека для измерений была в целом неточная) и множество тестов показали, что булевые операции с not оказывались медленнее чем == true или == false.
На деле это конечно же неправда и как минимум идёт в разрез с обще поставленной логикой, а если точнее - чем меньше байткода, тем быстрее он выполнится. Посмотрим же на приведённый пример.
» Пример
На скриншоте мы видим две булевые операции not true и true == false, рассмотрим байткод:
Байткод (not true):
0C510800 - literal
00000001 - value
25610000 - operator not
00000000
2A510000 - jump if
00001483 - register
16000000 - call jass
00000A93 - jass function
2B000000 - jmp
00001484 - register
28000000 - label
00001483 - register
28000000 - label
00001483 - register
0C000000 - literal
00000000
27000000 - retn
Байткод (true == false):
0C510800 - literal
00000001 - va
13510000 - push 0x51
00000000
14530000 - pop 0x53
00000000
1A535352 - eq 0x53 0x53 0x52
00000000
2A530000 - jmpf
00001483 - where to
16000000 - call jass
00000A93 - func id
2B000000 - jmp
00001484 - where to
28000000 - label
00001483 - id
28000000 - label
00001484 - id
0C000000 - literal
00000000 - retval
27000000 - ret
Как вы можете увидеть, операция через not меньше не только по байткоду, но и по выделенным "данным", ибо в варианте true == false мы выделяем 2 литерала, один под true, другой под false и третий под результат, а для получения результата используется дополнительная команда eq (equals - равно).
Собственно выходит, что чем короче булевые операции, чем меньше в целом действий, тем она будет быстрее (что как обычно - логично).

TriggerAddAction утекает!

» раскрыть
Очередной древний миф, который опять же пробрался к нам с далёких просторов hiveworkshop и был так же "перепроверен" на xgm. Однако, хоть этот миф и правдив, но правда немного в другом и виной тому, как обычно "божественные пальчики" программистов Blizzard Entertainment.
Начну с краткого, нет, сама TriggerAddAction не при чём, виной всему TriggerClearActions и DestroyTrigger, которые просто на просто не имеют кода деструктора самого Action, а вот TriggerRemoveAction его имеет! А это значит, если вы сохраните triggeraction в переменную и удалите его через TriggerRemoveAction, то никакой "утечки" не будет.
Перейдём к самим тестам!
» Проверка "утечек"
Для начала проверим количество созданных хендлов после: CreateTrigger, TriggerAddAction и TriggerAddCondition.
Как видите, всего добавилось 30000 хендлов, ибо каждый цикл +3 хендла и всего 10000 циклов.
» Посмотрим результат после DestroyTrigger:
Как вы можете заметить, удалилось лишь 20000 хендлов, вместо 30000, но почему же? Хоть я и дал ответ ранее, стоит рассмотреть эту проблему подробнее.
Давайте же посмотрим строго на TriggerAddAction и что же будет после DestroyTrigger:
» Результат после DestroyTrigger
Как вы видите, хендлов всё так же 10000, а это значит, что TriggerRemoveActions не работает. Но почему же? - спросите вы, чтобы ответить на этот вопрос нужно для начала посмотреть на TriggerRemoveConditions, почему же он работает?
» TriggerRemoveConditions
Большинству из вас не будет понятно куда нужно смотреть и что это вообще такое, я упрощу вам жизнь, просто запомните вот это:
(*(void (__stdcall **)(int, int))(**(uint32_t **)((signed int)result <= 0 ? 8 : result + 2) + 0x5C))( ); // CAgentWar3::Destroy
» TriggerClearActions
Ничего не заметили? Если заметили, то можете похлопать себя по плечу, я горжусь вами! А если нет, то я вас понимаю, ибо разница очень и очень МАЛЕНЬКАЯ и заключается как раз в
(*(void (__stdcall **)(int, int))(**(uint32_t **)((signed int)result <= 0 ? 8 : result + 2) + 0x5C))( ); // CAgentWar3::Destroy
которой тут просто... нет... а это значит, что хендл triggeraction просто на просто не удаляется и остаётся в памяти игры.
Итог: утекает не TriggerAddAction, а отсутствие удаление деструктора triggeraction в DestroyTrigger и TriggerClearActions.

Информация:

Что нужно для проверки скорости функций и прочего?

Всё достаточно просто, просто скачайте мой MemHackAPI отсюда: xgm.guru/p/wc3/memoryhackapi
В папке UselessTesting будет триггер Testing в нём есть функция TestBenchmarking, в первый и второй цикл вставьте функции, что вы хотите сравнить. Когда оба цикла завершается, будет высвечено сколько каждый из циклов занял в мс (миллисекунды). Оба цикла делаются 10000 раз, чтобы получить большее число без умножений и неточностей. Собственно, чтобы получить одиночную задержку, надо разделить полученное число на 10000 или установленное Вами значение.

Заключение:

За свою долгую историю, Warcraft 3 пожалуй создал пожалуй самое мощное начало для мододелов, что повлекло за собой уйму отдельных игр, которые ранее были картами в этой игре. Зачастую, многие создатели (включая меня) сильно недооценивали Jass в целом и всячески пытались найти тот или иной повод упрекнуть его в той или иной проблеме, не проведя нужных проверок, чтобы выявить реальную причину.
К моему большому сожалению, по сей день большинство картоделов опираются на те или иные мифы, которые услышали на Hive, а некоторые услышали что-то на xgm и других сайтах, и вроде как не от каких-либо случайных людей, а по факту проверенных, однако даже они могут ошибаться.
Потому, я очень надеюсь, что даже бегло прочитав мою статью, она развеет сложившиеся ложные впечатления о Jass и эти вопросы навсегда отпадут.
2
Голосов: 2
2
Голосов: 2
Ruti Ragnason - 2 weeks ago
2
Голосов: 2
Помню бомбящих милишников и любителей 8 мб со времен никому не нужной гарены, которые мне рассказали, как HD модели очень сильно и упорно просаживают FPS, а при достижении 100 юнитов игра не выдерживает и фаталит. Отправился к Awasy, попросил сконвертировать модель, попался по рофлу половой орган. Он имел около 15К (полигонов, вершин, точно не помню) натыкал он их около 100 штук - оказывается, ничего не лагает, но вершин там просто дофига и сама текстура, как и модель, небыли оптимизированны. Сразу завода сконвертированы mdx.
Ну и глядя на многие китайские карты, где под 140-150 FPS у китайцев на экране, HD контент сильно не просаживают. Потому что практически весь декор из каких-то мобильных игрушек или мультяшных игр, где вес смешной, но вид декора конечно выглядит бомбо.
0
Голосов: 0
Unryze - 2 weeks ago
0
Голосов: 0
Ruti Ragnason:
Ну, люди всё ещё думают, что граф движок как-то связан с синхом и прочим, хотя на деле игре дико до шляпы, прорисовалось ли что-то или нет. Фактически моя ВФЕ тому прямое доказательство, я могу рисовать локально фреймы, модели и т.д. и всё нормально. По желанию можно как в RenderEdge вообще отключить те или иные прорисовки и опять же не будет никакого эффекта.
Но... мифы породили и они устаканились. :(
2
Голосов: 2
PT153 - 2 weeks ago
Edited by
2
Голосов: 2
Unryze, код всегда кидай в форматировании, у тебя строки кода из C++ без него из-за чего текст в мифе про TriggerAddAction нечитаем.
И я так и не понял, почему мы смотрели код TriggerRemoveCondition, а не TriggerRemoveAction.

Как вы видите, хендлов всё так же 10000, а это значит, что TriggerRemoveActions не работает.
Так конечно функция не работает, ведь она закоментированна.
Итого: утекает не TriggerAddAction, а отсутствие удаление деструктора triggeraction в DestroyTrigger и TriggerClearActions.
Отсутствие удаление деструктора? Либо отсутствие деструктора, либо отсутствие удаления.

Вообще рекомендую каждый пункт разбить на подстатьи, так будет намного проще исправлять их.
0
Голосов: 0
Unryze - 2 weeks ago
0
Голосов: 0
Unryze, код всегда кидай в форматировании, у тебя строки кода из C++ без него из-за чего текст в мифе про TriggerAddAction нечитаем.
Поправлю, не ожидал что так поплывёт.
И я так и не понял, почему мы смотрели код TriggerRemoveCondition, а не TriggerRemoveAction.
Ну, для начала это были TriggerClearConditions и TriggerClearActions, и как раз разница в 1 строчку (отсутствие деструктора агента внутри TriggerClearActions), что и вызывает эту проблему. TriggerRemoveAction не имеет проблемы, но я его опустил, ибо кто будет сохранять triggeraction в переменную, чтобы потом её удалить?

Как вы видите, хендлов всё так же 10000, а это значит, что TriggerRemoveActions не работает.
Так конечно функция не работает, ведь она закоментированна.
Немного не тот скрин, но DestroyTrigger удаляет Conditions, но не Actions, ибо вызываются TriggerClearConditions и TriggerClearActions, но ради правильности добавлю правильный скрин, а то да, путаница.

Вообще рекомендую каждый пункт разбить на подстатьи, так будет намного проще исправлять их.
Есть такое, но мне этот txt2 уничтожает глаза и так, я привык к старому доброму BB-code. :(
0
Голосов: 0
МрачныйВорон - 2 weeks ago
0
Голосов: 0
not a > 4 или a <= 4
0
Голосов: 0
Unryze - 2 weeks ago
Edited by
0
Голосов: 0
not a > 4 или a <= 4
a <= 4 будет быстрее, ибо операция опкода lessOrEqual, а not a > 4 будет -> not Bigger то бишь две операции, но чего уж там, вот сравнение скорости:
Правда пришлось количество операций с 10к до 100к поднять, говоря проще, разница в скорости .001 - .01 нс или говоря ещё проще - фантомная.