• 🏆 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] Hashtables - Am I Doing It Right?

Status
Not open for further replies.

q.b

q.b

Level 3
Joined
Apr 21, 2014
Messages
21
I've been working with hashtables for quite a while now, but it occurred to me that I may not be using it properly. I may be missing or overlooking something. Just in case, can someone check my code if I'm doing it correctly?

Here's a sample of one of my abilities that uses hashtables, an item effect that restores HP over time to a unit that kills another unit.
JASS:
// Assuming that udg_hash is already initialized

function SoulCatcher_C takes nothing returns boolean
    return (UnitHasItemOfTypeBJ(u, 'I000')) and (IsUnitEnemy(GetTriggerUnit(), GetOwningPlayer(GetKillingUnit()))) and (IsUnitType(GetTriggerUnit(), UNIT_TYPE_STRUCTURE) == false)
endfunction

function SoulCatcher takes nothing returns nothing
    local trigger trg = GetTriggeringTrigger()
    local integer id = GetHandleId(trg)
    local real dur = LoadReal(udg_hash, id, 2)
    local unit u
    if (dur > 0) then
        set u = LoadUnitHandle(udg_hash, id, 0)
        call SetUnitState(u, UNIT_STATE_LIFE, GetUnitState(u, UNIT_STATE_LIFE) + (LoadReal(udg_hash, id, 1) * 0.10))
        call SaveReal(udg_hash, id, 2, dur - 0.10)
    else
        call DestroyTrigger(trg)
        call FlushChildHashtable(udg_hash, id)
    endif
    set trg = null
    set u = null
endfunction

function SoulCatcher_A takes nothing returns nothing
    local trigger trg = CreateTrigger()
    local integer id = GetHandleId(trg)
    call SaveUnitHandle(udg_hash, id, 0, GetKillingUnit())
    call SaveReal(udg_hash, id, 1, GetUnitState(GetTriggerUnit(), UNIT_STATE_LIFE) * 0.25)
    call SaveReal(udg_hash, id, 2, 1.00)
    call TriggerRegisterTimerEvent(trg, 0.10, true)
    call TriggerAddAction(trg, function SoulCatcher)
    set trg = null
endfunction

function InitTrig_map takes nothing returns nothing
    local trigger trg = CreateTrigger()
    call TriggerRegisterAnyUnitEventBJ(trg, EVENT_PLAYER_UNIT_DEATH)
    call TriggerAddCondition(trg, Condition(function SoulCatcher_C))
    call TriggerAddAction(trg, function SoulCatcher_A)
    set trg = null
endfunction

Am I doing it right, the basics, at least?
 

q.b

q.b

Level 3
Joined
Apr 21, 2014
Messages
21
Initializing hashtables is very expensive, and, in general, maps don't use even a fraction of their capacity.

That's why people suggest using an abstraction like Table - one shared hashtable per map, utilized by many resources.

Wow, thanks for the fast answer! However, I can't understand vJASS just yet to be able to use that Table 3.1, but I can see your point about hashtables.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
Do not make a new trigger all the time.
Instead use a timer the same way you used a trigger.
You can find some timer tutorials in here but even then they are not that much different except that they can only have one function as callback and a trigger can have many conditions and actions.

But to answer your question, yes you do use the hashtables correctly.
Using the same hashtable for multiple spells is indeed way better and I would even suggest using systems instead of making workaround for each spell, but that is another perspective.

In this case, you use 1, 2 and 3.
I suggest you to make a hashtable and call it "HashtableSpell" make global integer variables called "HSI_SoulCatcher_Killer", "HSI_SoulCatcher_Life" and "HSI_SoulCatcher_Duration" and give them the value 0, 1 and 2.

Now you use those global variables instead of the raw 0, 1 and 2 in your triggers and it will work on dynamic indexes.
Make a trigger where you initialize the hashtable and make a comment about which indexes are used.

When you make a different spell that requires a hashtable, make new global variables and start with 3.
So he indexes for that spell will be 3, 4, 5 and 6 (depending on how many data the spell requires.)
That way, you practically do the same as Table.

(There are maximum 255 hashtables in a map.)
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
Normally you build yourself convenience functions, so you do not have to state the same hashtable over and over again and you do not use vacuous literal keys, at least not integer but instead constant named variables or vJass has the "key" feature, generating unique integers. When learning about classes/structs, you will see that you usually try to keep hashtable entries to a minimum. You rarely store reals but indexes of object instances instead and those objects hold properties through arrays.

Stuffing everything in a single hashtable is not effective either, yet that will only be noticeable after some ten thousand entries. My map uses a range of hashtables as well as a few for special tasks but only behind the scenes, you should not have to expose that to your triggers and have to do it manually.
 

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,522
Stuffing everything in a single hashtable is not effective either, yet that will only be noticeable after some ten thousand entries.

Citation needed. I don't think anyone has actually verified that. I believe the hashmaps used to implement hashtable are static, very large, and have pretty good anti-collision properties.
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
JASS:
function b takes nothing returns nothing
    local integer i = 5000

    loop
        exitwhen (i < 0)

        call SaveInteger(udg_ht, udg_c, udg_c, udg_c)

        set udg_c=udg_c+1

        set i=i-1
    endloop
endfunction

function a takes nothing returns nothing
    local integer i = 50

    call PreloadGenStart()

    loop
        exitwhen (i<0)

        call TriggerEvaluate(udg_t)

        set i=i-1
    endloop

    call PreloadGenEnd("benchmark.txt")

    call BJDebugMsg("end")
endfunction
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
PreloadGenStart clears the preload buffer and starts a timer. PreloadGenEnd outputs the preload lines in a specified file path (in this case Warcraft III\Logs\benchmark.txt) and also the elapsed time since PreloadGenStart. Here, I am just using it to measure how long everything between PreloadGenStart and PreloadGenEnd takes. With grimoire, you may use stopwatch instead.
 

Cokemonkey11

Code Reviewer
Level 29
Joined
May 9, 2006
Messages
3,522
JASS:
function b takes nothing returns nothing
    local integer i = 5000

    loop
        exitwhen (i < 0)

        call SaveInteger(udg_ht, udg_c, udg_c, udg_c)

        set udg_c=udg_c+1

        set i=i-1
    endloop
endfunction

function a takes nothing returns nothing
    local integer i = 50

    call PreloadGenStart()

    loop
        exitwhen (i<0)

        call TriggerEvaluate(udg_t)

        set i=i-1
    endloop

    call PreloadGenEnd("benchmark.txt")

    call BJDebugMsg("end")
endfunction

Yeah that isn't really useful, what you want to know is how fast the hashtable responds to requests and pushes, when it has little data, compared to when it has a lot.

I believe you'd need to use SharpCraft timers
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
You wanted evidence that the hashtable slows down inserting new entries the more entries it already contains. A few runs of this are evident.

For me it takes:

0.8 secs
1.6 secs
2.3 secs
3.1 secs
4.0 secs

So approximately 0.8 secs more per run. The framework burden is marginal (0.1 secs). It stays at 0.8 seconds absolute when I use a new hashtable everytime (holding previous ones in background).
 
Level 26
Joined
Aug 18, 2009
Messages
4,097
It's a big loop, the trigger evaluation is so that the thread does not halt prematurely. udg_c is increased continuously, it always saves under a new pair of keys, gradually filling the hashtable. Of course, here it's not just "some" 10k but 250k per initiation. I start it via esc key (not shown), then look into the benchmark.txt output for the run time. That's not very precise but obvious enough to prove the point.

Rather than caring if you use a single hashtable vs multiple ones, your algorithms should be effective. For example a FlushChildHashtable is a lot faster than mass RemoveSavedXXX calls. Or I believe overwriting an entry with null may be better than removal/reinsertion if you are using the same key combination a lot.
 
Level 24
Joined
Aug 1, 2013
Messages
4,657
I think it would make more sense to do this:
In this case you can see the difference better... I suppose.
Ofcourse it will be a very small difference if 250k makes such a small difference already but that proves that the increasing delay in saving of stuff into one hashtable is pretty negligible.
JASS:
function a takes nothing returns nothing
    local integer i = 50
    
    loop
        exitwhen (i<0)

        call TriggerEvaluate(udg_t)

        set i=i-1
    endloop
    
    call PreloadGenStart()
    
    call SaveInteger(udg_ht, udg_c, udg_c, udg_c)
    
    call PreloadGenEnd("benchmark.txt")
    
    call BJDebugMsg("end")
endfunction
 
Level 12
Joined
Feb 22, 2010
Messages
1,115
Well, I guess it is natural to crash if you store 3 millions amount of 32 bit data.
 
Status
Not open for further replies.
Top