У каждого в жизни наступает такой момент, когда нужно совершить действия с группой юнитов. Сейчас мы рассмотрим все способы взаимодействия с группой и сравним их производительность.
Переборы групп преследуют разные цели, но их можно свести к нескольким типам:
Чтоб использовать более менее приближённый к реалиям тест, будем использовать таймер, срабатывающий тысячу раз сто раз в секунду:
local n, g, c = 0, garbage('count'), clock()
TimerStart(CreateTimer(), 0.01, true, function()
	n = n + 1
	
	if n > 1000 then
		print(round((clock() - c) * 1000), round((garbage('count') - g) * 1024), n)
		PauseTimer(GetExpiredTimer())
		DestroyTimer(GetExpiredTimer())
		return
	end
	
	local group = CreateGroup()
	GroupEnumUnitsOfPlayer(group, player)
	
	--actions
	
	GroupClear(group)
	DestroyGroup(group)
end)
Заодно можно измерить его показатели используя миллисекунды и байты.
время память
10460 85420
Намаявшись перезапускать этот тест, я заметил, что время колебалось в пределах 10350-10500, а память всегда была равна 85420. Поэтому можно смело внести поправку:
print(round((clock() - c) * 1000) - 10300, round((garbage('count') - g) * 1024) - 85420, n)
время память
160 0
Теперь осталось определиться с действиями над юнитами при переборе, чтоб возможный оптимизатор не прибил цикл.
if UnitAlive(target) then BlzSetUnitName(target, n) end

ForGroup

---@param whichGroup group
---@param callback code
function ForGroup(whichGroup, callback) end
Самая первая функция, с которой сталкиваются при переводе кода из GUI в JASS.
ForGroup(group, function()
	local target = GetEnumUnit()
	if UnitAlive(target) then BlzSetUnitName(target, n) end
end)
время память
130 37181
Анонимную функцию, передаваемую в callback можно конечно вынести за пределы таймера, но тогда единственный вариант передать n в BlzSetUnitName(target, n) это сделать n глобальной. Так как мы не учим детей плоому есть более лучшие решения, можно просто запомнить и никогда не использовать ForGroup.

FirstOfGroup

---@param whichGroup group
---@return unit
function FirstOfGroup(whichGroup) end
Возвращает первый элемент группы или nil если группа пуста. В связи с этим возник хитрый способ: запускать бесконечный цикл и удалять юнита из группы. Недостаток которого очевиден - группу нельзя перебрать дважды. Как и у любого другого популярного метода, у него возникло кучу вариантов на любой вкус:
while true do
	local target = FirstOfGroup(group)
	if target == nil then break end
	if UnitAlive(target) then BlzSetUnitName(target, n) end
	GroupRemoveUnit(group, target)
end
local target = FirstOfGroup(group)
while target ~= nil do
	if UnitAlive(target) then BlzSetUnitName(target, n) end
	GroupRemoveUnit(group, target)
	target = FirstOfGroup(group)
end
repeat
	local target = FirstOfGroup(group)
	if target ~=nil and UnitAlive(target) then BlzSetUnitName(target, n) end
	GroupRemoveUnit(group, target)
until target == nil
время память
130 35893

BlzGroupUnitAt

---@param whichGroup group
---@param index integer
---@return unit
function BlzGroupUnitAt(whichGroup, index) end
Как долго сообщество ждало этой функции. Она позволяет получить юнита по индексу. Но чтоб максимально эффективно её использовать, нам понадобится ещё одна функция, назначение которой можно понять из названия:
---@param whichGroup group
---@return integer
function BlzGroupGetSize(whichGroup) end
Соединив их вместе можно замерить производительность:
for index = BlzGroupGetSize(group) - 1, 0, -1 do
	local target = BlzGroupUnitAt(group, index)
	if UnitAlive(target) then BlzSetUnitName(target, n) end
end
время память
130 35893
Преимущество этого способа в том, что вам не нужно очищать группу после перебора, не используются callback функции и из-за того, что группа перебирается с конца вы можете прям в цикле удалить из неё юнита не боясь смещения.

Заключение

Заключение прекрасно написал Bergi_Bear к предыдущей версии статьи:
Нет никакой разницы каким вы способом будете перебираться своих 20 юнитов
Но можно оптимизировать код, например вынеся создание группы до создания таймера:
local group = CreateGroup()
local n, g, c = 0, garbage('count'), clock()
TimerStart(CreateTimer(), 0.01, true, function()
	n = n + 1
	
	if n > 1000 then
		DestroyGroup(group)
		print(round((clock() - c) * 1000) - 10300, round((garbage('count') - g) * 1024) - 85420, n)
		PauseTimer(GetExpiredTimer())
		DestroyTimer(GetExpiredTimer())
		return
	end
	
	GroupEnumUnitsOfPlayer(group, player)
	
	-- actions
	
	GroupClear(group)
end)
метод время память
ForGroup 120 -47419
FirstOfGroup 120 -48707
BlzGroupUnitAt 120 -48707
Напоследок можно вспомнить слова Martin Golding:
Всегда пишите код так, будто сопровождать его будет склонный к насилию психопат, который знает, где вы живете.
... и перебирать группы как вам душе угодно.
Код
--@author https://xgm.guru/p/wc3/for-group
do
	local InitGlobalsOrigin = InitGlobals
	function InitGlobals()
		InitGlobalsOrigin()
		local garbage = collectgarbage
		local clock   = os.clock
		local player  = Player(PLAYER_NEUTRAL_PASSIVE)
		
		local function round(num)
			if num >= 0 then return math.floor(num + .5)
			else return math.ceil(num - .5) end
		end
		
		for i = 1, 1000 do
			CreateUnit(player, FourCC('hfoo'), 0, 0, 0)
		end
		
		local n, g, c = 0, garbage('count'), clock()
		TimerStart(CreateTimer(), 0.01, true, function()
			n = n + 1
			
			if n > 1000 then
				print(round((clock() - c) * 1000) - 10300, round((garbage('count') - g) * 1024) - 85420, n)
				PauseTimer(GetExpiredTimer())
				DestroyTimer(GetExpiredTimer())
				return
			end
			
			local group = CreateGroup()
			GroupEnumUnitsOfPlayer(group, player)
			
			--actions
			
			GroupClear(group)
			DestroyGroup(group)
		end)
		
		--[[
		ForGroup(group, function()
				local target = GetEnumUnit()
				if UnitAlive(target) then BlzSetUnitName(target, n) end
		end)
	
		while true do
			local target = FirstOfGroup(group)
			if target == nil then break end
			if UnitAlive(target) then BlzSetUnitName(target, n) end
			GroupRemoveUnit(group, target)
		end
		
		local target = FirstOfGroup(group)
		while target ~= nil do
			if UnitAlive(target) then BlzSetUnitName(target, n) end
			GroupRemoveUnit(group, target)
			target = FirstOfGroup(group)
		end
		
		repeat
			local target = FirstOfGroup(group)
			if target ~=nil and UnitAlive(target) then BlzSetUnitName(target, n) end
			GroupRemoveUnit(group, target)
		until target == nil
		
		for index = BlzGroupGetSize(group) - 1, 0, -1 do
				local target = BlzGroupUnitAt(group, index)
				if UnitAlive(target) then BlzSetUnitName(target, n) end
		end
		
		]]
	
	end
end
`
ОЖИДАНИЕ РЕКЛАМЫ...
22
Причина подребления памяти ForGroup проста - использование функций
Можно подробности?
24
Анонимная функция получит и через замыкание, а вот для именованой придётся заламлять глобальное пространство имён, что не очень хорошо.
Формулировка не совсем верная, ведь именованная локальная функция тоже получит замыкание на свою область видимости - замыкания никак не связаны с анонимностью и именованностью.
30
Можно подробности?
Если в кратце, то функция является объектом, на создание которого и выделяется память. Так же все переменные из области видимости попадают в замыкание, на которое тоже нужна память.
Формулировка не совсем верная, ведь именованная локальная функция тоже получит замыкание на свою область видимости - замыкания никак не связаны с анонимностью и именованностью.
Дополнил
Обычно в функцию перебора юнитов нужно передать дополнительные аргументы, например кастера способности. Анонимная функция получит его в замыкании из своей области видимости:
local caster = 'caster'
ForGroup(whichGroup, function()
	print(caster) --> переменная доступна и будет выведено "caster"
end)
Именованные функции используют для того, что вызывать их из разного места кода, и потому часто случается такая ситуация
ForGroupFunc = function() -- функция глобальна для использования из разных участков кода
	print(caster) --> здесь переменная не видна ибо в области видимости функции  её нет и будет выведено "nil"
end
		
local caster = 'caster' -- чтоб передать эту переменную в функцию, нужно её сделать глобальной
ForGroup(whichGroup, ForGroupFunc)
24
Доберусь до вара - проведу свои собственные измерения. Однократный перебор двух тысяч юнитов не самый показательный тест, имхо. Плюс не стоит забывать про трюк с заносом нативок в локалки, что позволяет выжать дополнительную производительность и недоступно при форгрупе.
30
Однократный перебор двух тысяч юнитов не самый показательный тест, имхо.
Какой тест будет показательным?
Плюс не стоит забывать про трюк с заносом нативок в локалки
Нужно всё от тестировать, даже итератор, который я правда ещё не написал(
24
Какой тест будет показательным?
Что-то более похожее на реальные условия в карте - много переборов маленьких групп. Заодно и доля занимаемая самим перебором в сравнении с вызовом нативок возрастет.
33
Я вот смотрю на цифры производительности и ничего не понимаю... Скорее всего я должен понять на сколько быстр тот или иной способ, но глядя на цифры, я вижу что нет никакой разницы
0.86 > 0.82, чё? это то самое что имеют ввиду когда говорят:
Способ №1 быстрее способа №2 на 0.00001 наносекунд?
В реалиях варкрафта как это применимо? кто в здравом уме будет перебирать 2000 юнитов когда игра на топ железе залагает при 100 юнитах на экране...
Статья замечательная, и показывается все известные (почти) на данный момент методы, но я здесь вижу лишь 1 посыл:

Нет никакой разницы каким вы способом будете перебирать своих 20 юнитов

Дополнение: а что по поводу перебора юнитов вообще без группы, допустим если юниты будет в массиве или в таблице?
30
Что-то более похожее на реальные условия в карте - много переборов маленьких групп. Заодно и доля занимаемая самим перебором в сравнении с вызовом нативок возрастет.
Будет время дополню тесты.
Нет никакой разницы каким вы способом будете перебираться своих 20 юнитов
Кому нет, тот и не будет читать, а мне вот есть разница, поэтому и начал тестировать.
33
NazarPunk, я прочитал, наконец-то открыл для себя суть способа BlzGroupUnitAt ,буду использовать его, и даже прекрасно понял о его полезности =) но есть огромное преимущество - после перебора группа не очищается
NazarPunk, Так что по скорости перебора массива 2000 юнитов циклом?
28
Нужно провести множество одинаковых тестов, тогда будет смысл, а то пока выходит, что GroupClear() магическим образом ускоряет перебор.
Однократный перебор двух тысяч юнитов не самый показательный тест, имхо.
Согласен, тут всё в пределах 0.8 - 0.9.
Также стоит проводить тесты параллельно, ПОСЛЕ старта игры, а не во время инициализации.
Загруженные файлы
30
Также стоит проводить тесты параллельно, ПОСЛЕ старта игры, а не во время инициализации.
А какая собственно разница?
28
NazarPunk, потому что до старта игры никто не будет перебирать группу. К тому же, ты работаешь с WC3, там всякий чёрт может быть.
Про параллельно я имел ввиду, чтобы каждый тест запускался за другим, и так несколько раз подряд, например раз 1000, где потом выведется среднее значение и самое частое значение для каждого способа. Вот в этом реально будет смысл.
А пока выходит то, что я написал в предыдущем комментарии.

Ресурс тупо пропал из ленты...
30
Дополнение: а что по поводу перебора юнитов вообще без группы, допустим если юниты будет в массиве или в таблице?
то уже работа с массивами, а не с группой.
Назар снял его с публикации =(
Всего-то статью переделывал. Теперь можно дальше рассуждать о её бесполезности))
24
Кложур меджик тайм!
function GlobalTimerCallbackOverUnit(unit)
  local u = unit  -- переменная привязаная к каллбеку
  return function() print(u) end -- код каллбека
end
...
local unit = GetSomeUnit()
TimerStart(...,GlobalTimerCallbackOverUnit(unit))
Троеточиями заменены не важные участки кода, которые были опущены для наглядности.
Код не проверен на реальной луа машине, но вроде должно работать, если я все правильно помню.
Преимущество над анонимными функциями в чистом виде - контролируемый кложур, содержащий только нужные нам данные. Минус - более медленный старт за счет дополнительного вызова функции создающей каллбек.
Чтобы оставить комментарий, пожалуйста, войдите на сайт.