• 🏆 Texturing Contest #33 is OPEN! Contestants must re-texture a SD unit model found in-game (Warcraft 3 Classic), recreating the unit into a peaceful NPC version. 🔗Click here to enter!
  • It's time for the first HD Modeling Contest of 2024. Join the theme discussion for Hive's HD Modeling Contest #6! Click here to post your idea!

JASS Benchmarking Results

JASS Benchmarking & Efficiency Results

Feel free to post your own tests or dispute one of mine. I'll be updating this thread with as much info as I can. Remember results may vary depending on your computer.

Much of this has already been known or could be assumed, but it's nice to have the actual numbers and a reference like this.

All tests are done with the StopWatch Natives.

Native comparisons
  • ExecuteFunc
    • It seems length of the function name significantly changes the execution time. With a lengths of 1 and 70, the function with a name of length 1 was ~13% faster
    • ~27% slower thanTriggerEvaluate - [Results]


  • TriggerEvaluate
    • ~87% slower than a function call - [Results]
    • ~25% faster thanTriggerExecutewhich leads us to believe thattriggercondition is more efficient thantriggeractionin terms of execution time.


  • UnitAlive
    • ~15-20% faster thanGetWidgetLife(u) > 0.405
    • ~47-50% faster thanIsUnitType(u, UNIT_TYPE_DEAD) or GetUnitTypeId(u) == 0


  • SetUnitPosition
    • ~90% slower thanSetUnitX&SetUnitY


  • SetWidgetLife
    • ~15-20% faster thanSetUnitState(u, UNIT_STATE_LIFE, 5)


  • GetWidgetLife
    • ~20% faster thanGetUnitState(u, UNIT_STATE_LIFE) - [Results]


  • GetWidgetX
    • ~7-9% slower thanGetUnitX
    • ~2-4% slower thanGetItemX

Other comparisons

  • Group Enumeration (Test Code)
    ForGroupis always the slowest way to enumerate through a unit group. TheFirstOfGroupmethod is the fastest except in cases where the unit group has less than a few units. In that case placing your actions inside the enum filter will prove to be considerably faster. In cases where you want to keep the unit group you should swap groups. See vJass Optimization: Using a First of Group Loop for Enumeration
  • Recursive Calls
    Recursive function calls (like many BJ's) have a relatively significant impact on performance. For exampleGetHandleIdwas ~24-28% faster thanGetHandleIdBJand it increases within each recursion.​
  • Hashtable vs. Game Cache
    Hashtables are faster, nothing too surprising here.

    All variables used in my tests were just 1 character long and they were globals as opposed to local. There are too many small tests to include here but they are simple and I'm sure there were no errors.​
    • SaveInteger(hashtable) was ~35-40% faster thanStoreInteger(game cache). The same goes forSaveStrvs.StoreStringwhile booleans may be a tiny bit slower.
    • SaveUnitHandle was only ~8-12% faster thanStoreUnitwhich is a bit odd.
    The same speeds mostly still apply for loading, respectively. However when comparingLoadUnitHandlevs.RestoreUnit the latter is ~40-45% slower.​


JASS:
library Stopwatch initializer onInit
    
    //===========================================================================
    
    //! textmacro STOPWATCH_BENCHMARK_LOCAL_VARIABLES
        local real c
    //! endtextmacro
    
    //! textmacro STOPWATCH_BENCHMARK_TEST_1
        set c = Pow(5, 2)
    //! endtextmacro 
    
    //! textmacro STOPWATCH_BENCHMARK_TEST_2
        set c = (5 * 5)
    //! endtextmacro 
    
    //! textmacro STOPWATCH_BENCHMARK_INIT
    //! endtextmacro
    
    globals
        private constant integer ITERATIONS = 1000 // how many times to run each test
        private constant string  TEST_1     = "Pow(5, 2)"
        private constant string  TEST_2     = "(5 * 5)"
        private constant boolean USE_SLEEP  = false // add TriggerSleepAction's between tests
        private constant boolean STACK      = true // stack iterations
        
        // personal variables here
    endglobals
    
    //===========================================================================
    
    globals
        private integer C=1 // stack count
        private real array R // results
    endglobals
    
    // Declare the StopWatch natives
    native StopWatchCreate  takes nothing returns integer
    native StopWatchTicks   takes integer id returns integer
    native StopWatchMark    takes integer id returns integer
    native StopWatchDestroy takes integer id returns nothing

    private function Actions takes nothing returns nothing
        local integer sw
        local integer i
        local string output = ""
        
        //! runtextmacro STOPWATCH_BENCHMARK_LOCAL_VARIABLES()
        
        // Disable the trigger so it can't be interrupted
        call DisableTrigger(GetTriggeringTrigger())
        
        static if (USE_SLEEP) then 
            call TriggerSleepAction(0) 
        endif
        
        set i  = 0
        set sw = StopWatchCreate() // Create the StopWatch
        
        loop
            exitwhen i == ITERATIONS // Run test #1 X amount of times
            //! runtextmacro STOPWATCH_BENCHMARK_TEST_1()
            set i = i + 1
        endloop
        
        // If stacking is enabled, the results will add
        static if (STACK) then
            set R[0] = R[0] + StopWatchTicks(sw)
            set R[3] = R[3] + StopWatchMark(sw)
        else
            set R[0] = StopWatchTicks(sw)
            set R[3] = StopWatchMark(sw) // the length in milliseconds it took to run
        endif
        
        call StopWatchDestroy(sw)
        
        static if (USE_SLEEP) then 
            call TriggerSleepAction(0) 
        endif
        
        set output = "|cff008200===========================================================================|r\n"
        set output =  output + "|cffffcc00" + I2S(ITERATIONS * C) + "|r iterations of " + TEST_1 + " took |cffffcc00" + I2S(R2I(R[3])) + "|r milliseconds to finish.\n"
        
        static if (USE_SLEEP) then 
            call TriggerSleepAction(0) 
        endif
        
        // Set the iteration count to 0
        set i  = 0
        set sw = StopWatchCreate()
        
        loop
            exitwhen i == ITERATIONS
            // This is where test #2 is being executed
            //! runtextmacro STOPWATCH_BENCHMARK_TEST_2()
            set i = i + 1
        endloop
        
        static if (STACK) then
            set R[1] = R[1] + StopWatchTicks(sw)
            set R[4] = R[4] + StopWatchMark(sw)
        else
            set R[1] = StopWatchTicks(sw)
            set R[4] = StopWatchMark(sw)
        endif
        
        call StopWatchDestroy(sw)
        
        // Enable the trigger
        call EnableTrigger(GetTriggeringTrigger())
        
        set output = output + "|cffffcc00" + I2S(ITERATIONS * C) + "|r iterations of " + TEST_2 + " took |cffffcc00" + I2S(R2I(R[4])) + "|r milliseconds to finish.\n\n"
    
        // Display the results
        if (R[0] > R[1]) then 
            set R[2] = 100 - (R[1]/R[0] * 100)
            set output = output + TEST_2 + " was " + R2S(R[2]) + "% |cff80ff80faster|r than " + TEST_1 + "\n\n"
        else
            set R[2] = 100 - (R[0]/R[1] * 100)
            set output = output + TEST_1 + " is " + R2S(R[2]) + "% |cff80ff80faster|r than " + TEST_2 + "\n\n"
        endif
        
        static if (STACK) then
            set C=C+1
        endif
        
        set output = output + "|cff008200===========================================================================|r\n\n\n\n\n"
        
        call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, output)
    endfunction

    private function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        local integer i = 0
        loop
            exitwhen i == 15
            call TriggerRegisterPlayerEvent(t, Player(i), EVENT_PLAYER_END_CINEMATIC)
            set i = i + 1
        endloop
        call TriggerAddAction(t, function Actions)
        //! runtextmacro STOPWATCH_BENCHMARK_INIT()
    endfunction
    
    //===========================================================================
    
endlibrary



JASS:
scope Stopwatch initializer onInit
    
    globals
        private constant integer ITERATIONS = 4000
    endglobals
    
    private function Actions takes nothing returns boolean
        local integer sw
        local integer i
        local real array ticks
        local string output

        set i  = 0
        set sw = StopWatchCreate()
            
        loop
            exitwhen i == ITERATIONS
            // TEST 1 HERE
            set i = i + 1
        endloop
            
        set ticks[0] = StopWatchTicks(sw)
        set output = I2S(ITERATIONS) + " iterations of Test #1 took " + I2S(StopWatchMark(sw)) + " milliseconds to finish.\n"
        call StopWatchDestroy(sw)
            
        set i  = 0
        set sw = StopWatchCreate()
            
        loop
            exitwhen i == ITERATIONS
            // TEST 2 HERE
            set i = i + 1
        endloop
            
        set ticks[1] = StopWatchTicks(sw)
        set output = output + I2S(ITERATIONS) + " iterations of Test #2 took " + I2S(StopWatchMark(sw)) + " milliseconds to finish.\n\n"
        call StopWatchDestroy(sw)
            
        if (ticks[0] > ticks[1]) then
            set ticks[2] = 100 - (ticks[1]/ticks[0] * 100)
            set output = output + "Test #2 was " + R2S(ticks[2]) + "% faster than Test #1\n\n"
        else
            set ticks[2] = 100 - (ticks[0]/ticks[1] * 100)
            set output = output + "Test #1 is " + R2S(ticks[2]) + "% faster than Test #2\n\n"
        endif
        
        call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, output)
        
        return false
    endfunction

    //===========================================================================
    private function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterPlayerEvent(t, Player(0), EVENT_PLAYER_END_CINEMATIC)
        call TriggerAddCondition(t, function Actions)
    endfunction

endscope
 
Last edited:
When it comes to death checks, it's not about speed, but correctness.

GetWidgetLife(u) > 0.405 can be true for a dead unit:

JASS:
local unit u = CreateUnit(Player(0), 'hpea', 0, 0, 0)
call SetWidgetLife(u, 0)
call SetWidgetLife(u, 100)
call DisplayTimedTextToPlayer(GetLocalPlayer(), 0, 0, 6000, R2S(GetWidgetLife(u)))

That'll print 100 while you have a dead peasant laying there, rotting like fuck.
 
EDIT http://www.hiveworkshop.com/forums/2456309-post18.html

It appears that in patch 1.26a SetUnitPosition is no longer slow.

In 1.24 it was ~95% slower than SetUnitX/Y, but in 1.26 it's ~15-20% faster



JASS:
scope Stopwatch initializer onInit
    
    globals
        private constant integer ITERATIONS = 5000
        real x
        item j = CreateItem('ratf', 0, 0)
        unit u = CreateUnit(Player(0), 'hfoo', 0, 0, 0)
    endglobals
    
    private function Actions takes nothing returns boolean
        local integer sw
        local integer i
        local real array ticks
        local string output = ""
        
        set i  = 0
        set sw = StopWatchCreate()
            
        loop
            exitwhen i == ITERATIONS
            call SetUnitPosition(u, 1000, 1000)
            set i = i + 1
        endloop
            
        set ticks[0] = StopWatchTicks(sw)
        call StopWatchDestroy(sw)
        
        //set output = I2S(ITERATIONS) + " iterations of Test #1 took " + I2S(StopWatchMark(sw)) + " milliseconds to finish.\n"
            
        set i  = 0
        set sw = StopWatchCreate()
            
        loop
            exitwhen i == ITERATIONS
            call SetUnitX(u, 1000)
            call SetUnitY(u, 1000)
            set i = i + 1
        endloop
            
        set ticks[1] = StopWatchTicks(sw)
        call StopWatchDestroy(sw)
        
        //set output = output + I2S(ITERATIONS) + " iterations of Test #2 took " + I2S(StopWatchMark(sw)) + " milliseconds to finish.\n\n"
            
        if (ticks[0] > ticks[1]) then
            set ticks[2] = 100 - (ticks[1]/ticks[0] * 100)
            set output = output + "Test #2 was " + R2S(ticks[2]) + "% faster than Test #1\n\n"
        else
            set ticks[2] = 100 - (ticks[0]/ticks[1] * 100)
            set output = output + "Test #1 is " + R2S(ticks[2]) + "% faster than Test #2\n\n"
        endif
        
        call DisplayTextToPlayer(GetLocalPlayer(), 0, 0, output)
        
        return false
    endfunction

    //===========================================================================
    private function onInit takes nothing returns nothing
        local trigger t = CreateTrigger()
        call TriggerRegisterPlayerEvent(t, Player(0), EVENT_PLAYER_END_CINEMATIC)
        call TriggerAddCondition(t, function Actions)
        set j = CreateItem('ratf', 0, 0)
    endfunction

endscope


JASS:
scope BenchmarkSetUnitPosition initializer Init

    globals
        private constant integer ITERATIONS = 8000
        private trigger t = CreateTrigger()
        unit u = gg_unit_Hblm_0001
    endglobals
    
    private function Actions takes nothing returns nothing
        local integer curLoop
        local integer sw      
        local real array result
        
        call DisableTrigger(t)
        call BJDebugMsg("\n\n") 
        
        // TEST 1
        set curLoop = 0
        set sw = StopWatchCreate()
        loop
            exitwhen curLoop >= ITERATIONS
            call SetUnitPosition(u, 0, 0)
            set curLoop = curLoop + 1
        endloop
        set result[0] = StopWatchMark(sw)
        call StopWatchDestroy(sw)
        
        call BJDebugMsg("SetUnitPosition: " + I2S(curLoop) +" iterations took " + R2S(result[0]))
        call PolledWait(1)
        
        // TEST 2
        set curLoop = 0
        set sw = StopWatchCreate()
        loop
            exitwhen curLoop >= ITERATIONS
            call SetUnitX(u, 0)
            call SetUnitY(u, 0)
            set curLoop = curLoop + 1
        endloop
        set result[1] = StopWatchMark(sw)
        call StopWatchDestroy(sw)
        
        call BJDebugMsg("SetUnitX/Y: " + I2S(curLoop) +" iterations took " + R2S(result[1]))
        
        if (result[0] < result[1]) then
            set result[2] = 100 - (result[0]/result[1] * 100)
            call BJDebugMsg("Test #1 was " + I2S(R2I(result[2])) + "% faster than Test #2")
        else
            set result[2] = 100 - (result[1]/result[0] * 100)
            call BJDebugMsg("Test #1 was " + I2S(R2I(result[2])) + "% slower than Test #2")
        endif
        
        call EnableTrigger(t)
    endfunction

    //===========================================================================
    private function Init takes nothing returns nothing
        call TriggerRegisterPlayerEvent(t, Player(0), EVENT_PLAYER_END_CINEMATIC)
        call TriggerAddAction(t, function Actions)
        set u = gg_unit_Hblm_0001
    endfunction

endscope

I thought this was a mistake, but when spamming SetUnitPosition in 1.24, I got lag. However there was no lag in 1.26a

That'll print 100 while you have a dead peasant laying there, rotting like fuck.

Good thing UnitAlive is fastest then;)
 
Last edited:
Level 18
Joined
Sep 14, 2012
Messages
3,413
I wrote fastly this :
JASS:
struct Test extends array
    //Parameters
	private thistype prev
	private thistype next
	private static integer count
	private static timer period
	
	method destroy takes nothing returns nothing
		if this.next != 0 then
			set this.next.prev = this.prev
		endif
		set this.prev.next = this.next
		set this.prev = thistype(0).prev
		set thistype(0).prev = this
		if thistype(0).next == 0 then
			call PauseTimer(period)
		endif
	endmethod
	
	private static method periodic takes nothing returns nothing
		local thistype this = thistype(0).next
		loop
			exitwhen this == 0
			//Do your stuff
			if NeedDestroy then
				call this.destroy()
			endif
			set this = this.next
		endloop
	endmethod
	
	static method add takes paramaters... returns nothing
		local thistype this
		if thistype(0).prev == 0 then
			set count = count + 1
			set this = count
		else
			set this = thistype(0).prev
			set thistype(0).prev = thistype(0).prev.prev
		endif
		if thistype(0).next == 0 then
			call TimerStart(period, 0.0312500, true, function thistype.periodic)
		else
			set thistype(0).next.prev = this
		endif
		set this.next = thistype(0).next
		set thistype(0).next = this
		set this.prev = thistype(0)
		//Set parameters
	endmethod
	
	private static method onInit takes nothing returns nothing
		set count = 0
		set period = CreateTimer()
	endmethod
endstruct

struct Test2 extends array
	//Your parameters
	private thistype recycle
	private static thistype array data
	private static integer dindex
	private static timer period
	private static integer count
	private static thistype recycleNext
	
	method destroy takes nothing returns nothing
		set recycleNext = recycle
		set recycle = this
	endmethod
	
	private static method takes nothing returns nothing
		local thistype this
		local integer i = 0
		loop
			exitwhen i>dindex
			set this = data[i]
			//Do your stuff
			if NeedDestroy then
				call this.destroy()
				set data[i] = data[dindex]
				set i = i - 1
				set dindex = dindex - 1
			endif
			if dindex == -1 then
				call PauseTimer(period)
			endif
			set i = i + 1
		endloop
	endmethod
	
	static method add takes parameters ... returns nothing
		local thistype this
		if recycle == 0 then
			set count = count + 1
			set this = count
		else
			set this = recycle
			set recycle = recycle.recycleNext
		endif
		//Set parameters
		set dindex = dindex + 1
		set data[dindex] = this
		if dindex == 0 then
			call TimerStart(period, 0.0312500, true, function thistype.periodic)
		endif
	endmethod
	
	private static method onInit takes nothing returns nothing
		set count = 0
		set recycleNext = 0
		set dindex = -1
		set period = CreateTimer()
	endmethod
endstruct

The first is with the linked list I mostly use (I hope I didn't make any mistake I made this fastly on Notepad xD) and the second is a common indexing.
You can shorten the variable's name to the same length for the test to be better.
 
Level 31
Joined
Jul 10, 2007
Messages
6,306
In 'real' programming languages yes (or most likely). But this is vJASS, and there are arrays behind linked lists (not plain pointers). Array reads are slow (in vJASS especially).

in both, arrays are faster to iterate over than lists

this has also been benched in JASS.


It's comparing

set i = i + 1
exitwhen i == 0

to

set i = n
exitwhen i == 0


what's faster?
i + 1
n


the first involves 1 lookup and 1 operation

the second involves 2 lookups and 1 operation (don't forget that n is n + i - proven on wc3c)
 
Last edited:
Level 16
Joined
Aug 7, 2009
Messages
1,403
It was clear to me that array wins in JASS (because of what you said), and come to think of it, I can understand why it's faster in, something like, C, for example. I haven't done this comparison before for real languages and didn't think it through before posting that.
 
Level 19
Joined
Mar 18, 2012
Messages
1,716
Okay then I had wrong informations. Once (until yesterday) we had a tutorial Introduction to Collections: Index Array, Linked List, Stack, Queue, Dequeue. And Nes wrote there:
Furthermore, looping through an indexed array in JASS is slower than looping through what is called a linked list as JASS math is so slow. Indexed arrays also take up minimal memory.
 
Last edited:
Level 16
Joined
Aug 7, 2009
Messages
1,403
What about when you want to keep the unit group?

Just use ForGroup. It's slower, but the speed difference isn't lethal, unless you're using it on a big unit group in a 1/32 second timeout timer (and it works just fine with smaller groups - my auras work like that and I have 60 FPS constantly with 10+ auras running simultaneously). It's better than any silly workaround that generates 5 times more code. You fairly rarely need to loop through the same unit group more than once (groups are mostly used for instant AoE enumeration), and it's acceptable in those cases. That's why we have it after all.
 
Just use ForGroup. It's slower, but the speed difference isn't lethal, unless you're using it on a big unit group in a 1/32 second timeout timer (and it works just fine with smaller groups - my auras work like that and I have 60 FPS constantly with 10+ auras running simultaneously). It's better than any silly workaround that generates 5 times more code. You fairly rarely need to loop through the same unit group more than once (groups are mostly used for instant AoE enumeration), and it's acceptable in those cases. That's why we have it after all.

Or he could use the HandleStack... [/advertisement]
 
Level 19
Joined
Aug 8, 2007
Messages
2,765
Also can someone test set i = i + 1 vs set this = this.next//set this = next[this] (one char lenght variable ofc)

It could be important for custom camera systems when looping over every playing player with an interval of ~0.01.

huh? i=i+1 will be faster because this.next is an array lookup
 
Top