Добавлен , опубликован
Раздел:
Основы

Вступление

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

За относительно долгое время, что я провёл работая с 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
Если же Вам всё это ничего не говорит (большинству скорее всего это ничего и не скажет), это банально значит, что локальные переменные достаточно близки к аргументам функций по логике, а аргументы обнулять не нужно. Однако если же был создан новый объект, то без обнуления локальной переменной внутриигровой счётчик хендлов повысится на 1 единицу.
Простой ответ - сами переменные не утекают ни при каких обстоятельствах и они преобразуются в нечто статичное, и им выделена память единожды. Потому, утечки вызываются фактически банальной проблемой логики в самом коде, то бишь вы создали хендл, а затем его не удалили и так может повторяться уйму раз. Но это не утечка, ибо тогда по этой логике call CreateUnit и так далее - утечки, ибо этот юнит просто создан и дальше нам нужно его найти, чтобы убить или удалить и так далее.
Видео примеры
Пример с юнитом:
Пример с локацией:
Пример с группой:
Примеры с аргументами функций:
1)
2)
Подводя итоги, можно смело сказать, что никакого "ужаса" от использования локалок попросту нету, конечно же это не отменяет факта, что даже их стоит использовать с умом и их обнуление просто на просто необходимо, чтобы занятые регистры были очищены когда функция закончится и будет выполнена 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
То бишь логика в том, что мы можем подать объект из локальной переменной в глобальную и далее уже без утечки провести с ней операцию.

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

раскрыть
Я думаю многие частенько встречались с обсуждениями скоростей нативок, и чаще всего под "удар" попадала GetUnitX против GetWidgetX, и общепринятно, что GetWidgetX быстрее чем GetUnitX. Однако, так ли это? Так давай же узнаем!
Для начала, я предложу Вам посмотреть псевдокод game.dll обоих функций:
Псевдокод GetUnitX и GetWidgetX
Как видите, он фактически идентичен, 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 медленные!

раскрыть
Это как вы скорее всего и догадались очередной миф, который базируется вокруг "идеи", по которой прямое умножение джасс переменных должно быть быстрее нативок, однако это не так. Давайте начнём с примеров:
Пример Deg2Rad
Давайте теперь рассмотрим Rad2Deg:
Пример Rad2Deg
Результат одинаковый в обоих случаях, но почему же? Для этого нам нужно посмотреть в обработчик JASM байткода, который ссылается на опкод 0x22 (multiplication):
Обработкич Байткода
А теперь посмотрим на Deg2Rad:
Deg2Rad Code
В итоге получается, что умножение фактически превосходит вызов нативки по общим операциям, но всё это сводится к погрешности, которая банально делает эти операции равными. Такая же учесть следует и за 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 - равно).
Собственно выходит, что чем короче булевые операции, чем меньше в целом действий, тем она будет быстрее (что как обычно - логично).

Trigger​Add​Action утекает!

раскрыть
Очередной древний миф, который опять же пробрался к нам с далёких просторов hiveworkshop и был так же "перепроверен" на xgm. Однако, хоть этот миф и правдив, но правда немного в другом и виной тому, как обычно "божественные пальчики" программистов Blizzard Entertainment.
Начну с краткого, нет, сама TriggerAddAction не при чём, виной всему TriggerClearActions и DestroyTrigger, которые просто на просто не имеют кода деструктора самого Action, а вот TriggerRemoveAction его имеет! А это значит, если вы сохраните triggeraction в переменную и удалите его через TriggerRemoveAction, то никакой "утечки" не будет.
Перейдём к самим тестам!
Проверка "утечек"
Для начала проверим количество созданных хендлов после: CreateTrigger, TriggerAddAction и TriggerAddCondition.
Как видите, всего добавилось 30000 хендлов, ибо каждый цикл +3 хендла и всего 10000 циклов.
Посмотрим результат после DestroyTrigger:
Как вы можете заметить, удалилось лишь 20000 хендлов, вместо 30000, но почему же? Хоть я и дал ответ ранее, стоит рассмотреть эту проблему подробнее.
Давайте же посмотрим строго на TriggerAddAction и что же будет после DestroyTrigger:
Результат после DestroyTrigger
Как вы видите, хендлов всё так же 10000, а это значит, что TriggerClearActions не работает. Но почему же? - спросите вы, чтобы ответить на этот вопрос нужно для начала посмотреть на TriggerClearConditions, почему же он работает?
TriggerClearConditions
Большинству из вас не будет понятно куда нужно смотреть и что это вообще такое, я упрощу вам жизнь, просто запомните вот это:
(*(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.

Kill​Sound​When​Done очищает sound хендл!

раскрыть
Не секрет, что в огромном количестве карт используются звуки, однако правильно ли они используются, почти во всех случаях - нет. Однако, виноваты в этом далеко не картоделы, а в очередной раз сами Blizzard.
Каламбур KillSoundWhenDone связан с тем, что да, оно удаляет выполняемый звук, однако оно не удаляет сам хендл, то бишь CSoundWar3 и так как нет нативки на прямую установку/замену звука, то остаётся висячий хендл.
Посмотрим на счётчик хендлов без и с KillSoundWhenDone:
KillSoundWhenDone Тест
Как видите на уровне хендлов разницы нет вообще, однако KillSoundWhenDone убивает сам звук, что всё-таки освободит память занятую непосредственно звуком.
А теперь посмотрим мою функцию RemoveSound, которая будет доступна в UjAPI начиная с 1.0.24.92 версии.
RemoveSound
По итогу, получается что используя стандартный метод вызова звука, а точнее:
function PlaySound takes string soundName returns nothing
    local sound soundHandle = CreateSound(soundName, false, false, true, 12700, 12700, "")
    call StartSound(soundHandle)
    call KillSoundWhenDone(soundHandle)
endfunction
Или же метод без утечки индекса:
function PlaySoundEx takes string soundName returns nothing
    local sound soundHandle = CreateSound(soundName, false, false, true, 12700, 12700, "")
    call StartSound(soundHandle)
    call KillSoundWhenDone(soundHandle)
	set soundHandle = null
endfunction
Мы всё-равно не решаем проблему утечки CSoundWar3, а это значит, что единственное верное решение (по-крайней мере на ванилле/рефе) - это каждый раз создавать уникальный звук для героя и т.д. и не удалять его через KillSoundWhenDone, а останавливать через StopSound( s, false, true ) и использовать его вновь, а не создавать новый, надясь на то, что он будет таки очищен.

Информация:

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

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

Заключение:

За свою долгую историю, Warcraft 3 пожалуй создал пожалуй самое мощное начало для мододелов, что повлекло за собой уйму отдельных игр, которые ранее были картами в этой игре. Зачастую, многие создатели (включая меня) сильно недооценивали Jass в целом и всячески пытались найти тот или иной повод упрекнуть его в той или иной проблеме, не проведя нужных проверок, чтобы выявить реальную причину.
К моему большому сожалению, по сей день большинство картоделов опираются на те или иные мифы, которые услышали на Hive, а некоторые услышали что-то на xgm и других сайтах, и вроде как не от каких-либо случайных людей, а по факту проверенных, однако даже они могут ошибаться.
Потому, я очень надеюсь, что даже бегло прочитав мою статью, она развеет сложившиеся ложные впечатления о Jass и эти вопросы навсегда отпадут.
`
ОЖИДАНИЕ РЕКЛАМЫ...
2
15
2 года назад
2
Ещё ходил такой слушок, что вызов функции из другой функции несёт дополнительную нагрузку на JASS-машину. Этот миф появился из-за bj-функций, которые в себе вызывают аналогичную нативную функцию.
2
20
2 года назад
Отредактирован Unryze
2
Ещё ходил такой слушок, что вызов функции из другой функции несёт дополнительную нагрузку на JASS-машину. Этот миф появился из-за bj-функций, которые в себе вызывают аналогичную нативную функцию.
Ну, это не совсем миф, но и не совсем точная формулировка. Всё сходится к байткоду, опять же. Рассмотрим такой вариант:
ClearTextMessages и
function ClearTextMessagesMy takes nothing returns nothing
    call ClearTextMessages( )
endfunction
Получаем:
Выходит, что добавилось 2 МС на 10000 повторах, что опять же логично, рассмотрим байткод:
вызов нативки
15000000
00000325
Вызов функции:
16000000
00000A10
Изначальный вызов (если наша функция напрямую вызывала бы код нативки, а не саму нативку, скорость была бы равна). Однако мы из функции затем вызываем уже саму нативку:
15000000
00000325
И того, у нас двойной вызов, то бишь:
16000000
00000A10
15000000
00000325
Что в итоге добавило 200 наносекунд на каждый вызов или же 2 мс в общем.
Думаю добавлю это в статью тоже, ибо это технически не миф, а факт, ибо обёртки всегда будут добавлять задержку, ибо добавляется байткод. :)
Загруженные файлы
2
15
2 года назад
2
Unryze, ну тут скорее тема обсуждения: насколько критично плодить обёртки ради читаемости и поддерживаемости кода)
2
20
2 года назад
2
Unryze, ну тут скорее тема обсуждения: насколько критично плодить обёртки ради читаемости и поддерживаемости кода)
Зависит от метода использования, фактически оно не несёт столь значительного замедления, чтобы отказываться, да и всё же большинство наработок - это не только обёртки, а полноценные функции. А вот использовать BJ обёртки, когда они не несут фактического отличия от нативок - это всё же глупость, как я считаю. То бишь если это SetUnitAbilityLevel и SetUnitAbilityLevelSwapped (зачем использовать BJ функцию, если разница лишь в местоположении аргументов).
А так читаемость > псевдоскорость, потому даже если нагрузка увеличится на 1000нс - это всё-равно жалкая разница, так как это не сравнится с тем, на сколько функции МХ медленные. Когда нативка 2-10мс на 10000 повторов, функция мх - 140-160 и порой больше. Потому все эти псевдозакосы про скорость - как по мне глупости. Но самая большая глупость всё же - это старая добрая дилемма хеша и структур... с которой я всё ещё мягко говоря в шоке, что кто-то умудрился без доказательств подтвердить разницу в скорости (которая даже не ощутима) и народ повёлся. :(
2
15
2 года назад
2
Unryze, про хэш-таблицу как я понял все мифы из этой статьи пошли: xgm.guru/p/wc3/hashtable
0
26
2 года назад
0
так а где развенчивание мифа про то что хеш таблица медленная? ты же это сам и показал, кек, лишь подтвердив что она реально медленнее
2
20
2 года назад
Отредактирован Unryze
2
Unryze, про хэш-таблицу как я понял все мифы из этой статьи пошли: xgm.guru/p/wc3/hashtable
Гляну, спасибо.
так а где развенчивание мифа про то что хеш таблица медленная? ты же это сам и показал, кек, лишь подтвердив что она реально медленнее
Как-то очень поверхностно что ли прочитал статью, процитирую кусочек: "Рассмотрим их фактические задержки, 3 мс / 10000 = 300 наносекунд и 13 мс / 10000 = 1300 наносекунд, то бишь борьба идёт за 1000 наносекунд, что является 0.001 мс или же 0.000001 секунды. Потому мне лично сложно представить, где эта разница даст о себе знать и как часто делается 10000 повторов или же вызовов чего-либо."
Ну и поясню немного подробнее, если сравнивать ровно 1 массив и ячейку хеша, то да, массив конечно быстрее. Но если дойти до структур, где итерация идёт на все 8192 значения (0 - 8191), то скорость структуры в РАЗЫ будет ниже, если очень нужно, могу даже замерить, но как не крути, модульность хеша + его простейший перенос делает его по всем характеристикам лучше, чем vJass структуры.

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

И маленькая поправка, есть системы на структурах, где есть "счётчик", который увеличивается и уменьшается, что конечно же убирает нужду в итерации 8192 раза, но даже так, будут лишние итерации, нужда в нахождении свободного индекса и т.д. В общем и там и там - свои минусы, но утверждать что Хеш прямо МЕДЛЕННЕЕ - ну не знаю даже. Опять же лучшее сравнение нативки вс МемХак функции, разница в 60-70 раз порой, но это не делает их прямо невозможными для пользования, ибо в реальной ситуации, подобные нагрузки недостижимы и эти псевдоразницы скорости - никогда себя не проявят.
0
26
2 года назад
0
Как-то очень поверхностно что ли прочитал статью, процитирую кусочек: "Рассмотрим их фактические задержки, 3 мс / 10000 = 300 наносекунд и 13 мс / 10000 = 1300 наносекунд, то бишь борьба идёт за 1000 наносекунд, что является 0.001 мс или же 0.000001 секунды. Потому мне лично сложно представить, где эта разница даст о себе знать и как часто делается 10000 повторов или же вызовов чего-либо."
ну а теперь возьми, да забей хеш таблицу различными данными. чем больше она занята тем больше займет поиск по ключу. а потом представь что это какая нибудь система движения, где кроме этого происходит карнавал другого рода, и уже это начинает вызывать определенные вопросы
Unryze:
Забыл добавить, что я писал не о просто "сткрутурах" в целом, а при применении их в MUI наработках. Ну и хочется добавить, что лимит строго по индексу - куда хуже, чем возможность использовать уникальный хендл объекта как ключ, что убирает мягко говоря геморрой индексации.
в рефе например лимит массива поднят. но если ты не хочешь париться с лимитами и ограничениями, то используешь луа
И маленькая поправка, есть системы на структурах, где есть "счётчик", который увеличивается и уменьшается, что конечно же убирает нужду в итерации 8192 раза, но даже так, будут лишние итерации, нужда в нахождении свободного индекса и т.д. В общем и там и там - свои минусы
все еще быстрее
2
20
2 года назад
2
Как-то очень поверхностно что ли прочитал статью, процитирую кусочек: "Рассмотрим их фактические задержки, 3 мс / 10000 = 300 наносекунд и 13 мс / 10000 = 1300 наносекунд, то бишь борьба идёт за 1000 наносекунд, что является 0.001 мс или же 0.000001 секунды. Потому мне лично сложно представить, где эта разница даст о себе знать и как часто делается 10000 повторов или же вызовов чего-либо."
ну а теперь возьми, да забей хеш таблицу различными данными. чем больше она занята тем больше займет поиск по ключу.
Хештаблица - это не массив. Сколько ты её не забивай, скорость не изменится. Откуда вы эти басни штампуете - не понимаю.
а потом представь что это какая нибудь система движения, где кроме этого происходит карнавал другого рода, и уже это начинает вызывать определенные вопросы
Опять же 300нс, или же 0.00000003 секунды погоды не делают, и опять же, как-то же игра всё держит на хештаблице внутри движка? :)
Забыл добавить, что я писал не о просто "сткрутурах" в целом, а при применении их в MUI наработках. Ну и хочется добавить, что лимит строго по индексу - куда хуже, чем возможность использовать уникальный хендл объекта как ключ, что убирает мягко говоря геморрой индексации.
в рефе например лимит массива поднят. но если ты не хочешь париться с лимитами и ограничениями, то используешь луа
Без обид, но этот реф мне лично не упал, и повышение лимита не меняет саму косячность структур, о чём я и писал и ты это опять так и не заметил.
И маленькая поправка, есть системы на структурах, где есть "счётчик", который увеличивается и уменьшается, что конечно же убирает нужду в итерации 8192 раза, но даже так, будут лишние итерации, нужда в нахождении свободного индекса и т.д. В общем и там и там - свои минусы
все еще быстрее
0.00000003 секунды и только если это единичный вызов, системы на структурах медленнее, ввиду лишних циклов и итераций, прочитай пожалуйста что я пишу внимательно.
Загруженные файлы
0
27
2 года назад
0
Unryze, что-то я уже начинаю сомневаться, всё-таки циклом перебирать массив лучше чем на каждый снаряд делать по таймеру и каждый раз что-либо доставать оттуда и сохранять, исключение хэш-таблица + структура, немного оптимизированный вариант
2
20
2 года назад
Отредактирован Unryze
2
Unryze, что-то я уже начинаю сомневаться, всё-таки циклом перебирать массив лучше чем на каждый снаряд делать по таймеру и каждый раз что-либо доставать оттуда и сохранять, исключение хэш-таблица + структура, немного оптимизированный вариант
Чем оно более оптимизированно? Когда таймеры можно утилизировать и вообще удалять и очищать. На крайний, окей, можно сделать и "структуру" на хеше, которая не будет особо уступать по скорости (вопрос только зачем оно).
Мне сделать тест на 1000 таймеров что ли? :D Просто если исходить из реального (а юзкейсы со снарядами - это 0.1%), то сложно дойти до случая, где нагрузка будет столь велика, что нужно будет бороться за наносекунды, что я и пытался пояснить. Я делал на мемхаке итерации куда более сложных функций, нагрузки которые достижимы лишь синтетически, потому на деле добраться до таких диких нагрузок практически невозможно...
2
8
2 года назад
Отредактирован goodlyhero
2
Замечу, что достаточно часто, одной из первых оптимизаций хэштаблицы, которые можно, а часто и полезно, использовать - замена стрингхэшэй на тип кей из вжаса и "Массивы" с камими-то большими смещениями, ну типа на 100к, для каждого набора тех данных, которые хочется хранить, этого примерно всегда будет достаточно, и даст какой-то плюс по производительности и количеству написанного кода.

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

да и в конце нужно удалять и очищать таймер, а переменные обнулять, половину из этого как раз структура + хэш-таблица нивелирует
2
20
2 года назад
Отредактирован Unryze
2
Замечу, что достаточно часто, одной из первых оптимизаций хэштаблицы, которые можно, а часто и полезно, использовать - замена стрингхэшэй на тип кей из вжаса и "Массивы" с камими-то большими смещениями, ну типа на 100к, для каждого набора тех данных, которые хочется хранить, этого примерно всегда будет достаточно, и даст какой-то плюс по производительности и количеству написанного кода.

Но предположу, что возможно, одной из вторичных затрат при использовании систем, в которых на каждый объект приходится отдельный таймер, и, которые так хорошо идут с хэштаблицами, являются затраты на старт отдельного потока при срабатывании таймеров, что по моему скромному мнению должно также требовать некоторых ресурсов.
По большей части согласен, а по поводу потока, ресурс будет выделяться всё-равно меньше, ибо идёт банальный вызов функции при тике таймера (таймеры находятся в общем массиве), то бишь 200нс (обычный вызов). Это к счастью не форгрупп, который создаёт ещё и булэкспр внутри движка и ещё кучу прелестных операций.
rsfghd:
Unryze, ну.. стоит не забывать о получении истекающего таймера, получение его айди, потом ещё повытаскивать значения оттуда, сохранить эти значения, уже немало так лишних действий

да и в конце нужно удалять и очищать таймер, а переменные обнулять, половину из этого как раз структура + хэш-таблица нивелирует
set p = GetHandleId( GetExpiredTimer( ) ) даёт:
8 мс задержки на 10000 повторов, то бишь 800 нс на каждый вызов, смотрим на простейший цикл допустим из 6 значений и результат с ним:
И бац, нагрузка уже больше. Далее добавляем проверки что "массив под этому индексу != null" и прочие финтифлюшки структур и теряем ещё больше скорости. Я же не просто так сделал функцию бенчмаркинга, чтобы при малейшем сомнении, каждый мог взять и всё сам проверить. :)
К слову если взять допустим меньше, допустим 3 значения:
Получаем в среднем неплохую разницу, но опять же, чем больше всякой мути в коде, тем разница будет расти. В случае с хешем, мы "теряем" производительность скорее на дополнительных таймерах, но проверить их влияние сложнее, ибо толком не за что цепляться.
В целом можно сойтись на компромиссе, что и то и то "приемлемо" и по скорости равно, где-то свои плюсы, где-то свои минусы, но итог как вы видите варьируется.
0
27
2 года назад
0
Unryze, ну простейший цикл из 6 значений всё-таки, таймер перемножить на 6, т.е. 8х6 и выйдет больше результат, пускай это и не в одном потоке должно быть

массив по индексу никто не проверяет на != null

может мне пример работы с циклом и одним таймером показать, вместо хэштаблицы как используют
2
20
2 года назад
Отредактирован Unryze
2
Unryze, ну простейший цикл из 6 значений всё-таки, таймер перемножить на 6, т.е. 8х6 и выйдет больше результат, пускай это и не в одном потоке должно быть

массив по индексу никто не проверяет на != null
Приведу в пример ту же систему снарядов, ты же что-то будешь проверять, чтобы убедиться что снаряд нужно двигать, верно? Я писал об этом, и привёл простой пример. По таймеру, оно работает отдельно и у него будет своя отдельная задержка, каждый таймер, как и триггер "выстреливает" отдельно. И нет, оно не будет напрямую множится, это нонсенс. Таймер даже на .0 срабатывает медленнее цикла, то бишь оно всегда будет нагружать "меньше".
Я просто не знаю как уже до вас донести, что эти басни в реальном мире сводятся на нет, и фактическая разница скорости на столько незначительна, что ну нельзя говорить о полном преимуществе структуры или же хештаблицы. Просто как по мне, хештаблица выигрывает удобностью (стрингхеш всё же позволяет удобно "обзывать" ключи), модульностью (чики-брики легко перенести) ну и в целом дополнительным API без танцов с бубном.

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

Я просто хочу увидеть от кого-то пример, где "медленность" хештаблицы наглядно даст о себе знать, я тут извините распинаюсь и кидаю тесты с пруфами, а в ответ ничего так и не получил, чтобы опровергло мои высказывания или же подтвердило бы правоту мнения по поводу структур. :(
0
27
2 года назад
Отредактирован rsfghd
0
Приведу в пример ту же систему снарядов, ты же что-то будешь проверять, чтобы убедиться что снаряд нужно двигать, верно?
это всё равно что после выгрузки юнита из хэш-таблицы проверять, что он != null

цикл не делается до 8192, оно от минимального и максимального значения, которое варьируется от кол-ва "запусков", если что-то не нужным становится, на его место ставят последнее, а максимальное значение снижают на единичку, ну и дырку в цикле закрывают
2
20
2 года назад
Отредактирован Unryze
2
> Приведу в пример ту же систему снарядов, ты же что-то будешь проверять, чтобы убедиться что снаряд нужно двигать, верно?
это всё равно что после выгрузки юнита из хэш-таблицы проверять, что он != null
в общем ты не понял что я сказал. У тебя есть цикл, который перебирает структуру, и есть проверка хоть какая-то, может ты делаешь без неё но большинство наработок проверяют допустим наличие юнита, флага, или длительности и т.д.
цикл не делается от 0 до 8191, он делается от минимального и максимального значения, которое варьируется от кол-ва "запусков", если что-то не нужным становится, на его место ставят последнее, а максимальное значение снижают на единичку
Зависит от наработок, да и я показал, что даже при 6 элементах уже по факту задержка не маленькая. В общем, я привёл доказательства, пояснил всё, что только можно было, теперь дело за тобой и другими проверить эти структуры, привести примеры с указанием скорости, ибо пока что это всё терпит без практического доказательства.
2
28
2 года назад
2
Загруженные файлы
3
20
2 года назад
Отредактирован Unryze
3
Вот вам кусок кода.
Взято отсюда.
Почти все мифы с хайва и пошли... да и их "таймер" зачастую выдавал бредовые значения, по которым у них GetWidgetX был быстрее GetUnitX, сейчас я это протестирую, не вопрос. И да, сравнение опять же не честное, ибо 'cnst' по байтам превосходит любой индекс массива. Поправка: надо было мне в начале посмотреть таки тред, там наоборот отстаивают позицию по хештаблицам и опровергают басни про её медлительность, спасибо в общем за ссылку.
Но чёрт с ним, вот результаты:
Возьмём поделим 2мс на 5000, получаем 0.0004мс, что является 400нс или же 0.0000004 секунды. Так как у нас +-1мс уходит на массив, то разница по факту 200нс или же 0.0000002 секунды. Потому я ещё раз спрашиваю, где вы почувствовали такую гигантскую разницу, когда даже 5000 итераций дают жалкую погрешность? И я повторюсь - это не честное сравнение и если взять напрямую системы, весы явно отойдут от структур.
Я надеюсь на этом вопросы отпали?
Ну и тем, кто хочет проверить самим результат, прикерил код для Мемхака:
П.С. даже Лич в целом разделяет мою точку зрения: "Minding that that been 5k operations and yet it took <10 ms, it barely matters in real life, but it does matter for every snowflake."
PT153 ещё раз спасибо за полезную ссылку, остальным бы не помешало её посетить и вчитаться. :)
2
20
2 года назад
2
Добавлен миф "Deg2Rad и Rad2Deg медленные!" к этому же мифу также относится и Pow, ответ в статье!
2
28
2 года назад
2
Pow( a, a ) в сравнении с a * a
a * a = a^2, а не a^a.
2
20
2 года назад
Отредактирован Unryze
2
> Pow( a, a ) в сравнении с a * a
a * a = a^2, а не a^a.
Это был краткий пример, где квадрат убирают и ставят умножение, пример dx * dx вместо Pow( dx, 2 ). Хотя правильнее было бы взять в пример 2 * 2 = Pow( 2, 2 ) и было бы меньше путаницы. В итоге я хотел написать одно, а написал чуть ли не ересь. :D Спасибо, поправил на прямой пример.
0
27
2 года назад
0
миф который я сам для себя придумал и даже не проверял: постоянно юзать Player( 0x00 ) медленее, чем обращаться к глобальной переменной этого игрока
может у кого-нибудь тоже такая мысль мелькала
2
20
2 года назад
Отредактирован Unryze
2
миф который я сам для себя придумал и даже не проверял: постоянно юзать Player( 0x00 ) медленее, чем обращаться к глобальной переменной этого игрока
может у кого-нибудь тоже такая мысль мелькала
Это не миф, тут опять же будет Player( 0 ) медленнее, ибо лишние действия:
GetHandleId берёт 2ms, push переменной ещё 2ms, потому pTemp (который Player( 0 ) присвоенный) по факту будет иметь нулевую задержку, так как getvar выполняется очень быстро.
А Player( 0 ) - это в начале вызов нативки, затем пуш числа, и затем уже setvar. Говоря проще, практически всегда, чем больше действий, тем больше выйдет байткода, в итоге потребуется больше времени на выполнение операции.
Загруженные файлы
Чтобы оставить комментарий, пожалуйста, войдите на сайт.