garrysmod icon indicating copy to clipboard operation
garrysmod copied to clipboard

Hook library rewrite (prioritization and performance enhancements)

Open meepen opened this issue 6 years ago • 24 comments

This is based on my rewrite from https://gist.github.com/meepen/5566c9598e0e610e5885d12fad2ec3a8

Notes:

  • This maintains almost 100% backwards compatibility with the current hook system, the only exception is that hook.GetTable() modifications won't add or remove hooks to run. I believe this will be fine because ULX does the same, so developers have come to expect it won't work on a majority of servers.
  • Adds priority to the hook.Add function (higher number = higher priority)
  • Adds hook.Priority (a table) with a single member NO_RETURN
  • NO_RETURN is NOT enforced in code but should be followed as a standard. It will always be ran first in the priority list.
  • Adds hook.GetPriority(event, name)
  • Adds hook.GetInternal() so people who want to irritate the hook library can do so if they want without rewriting it or replacing it with a whole new library. Please don't document this on the wiki, or make it say it is internal and should not be used.
  • Adds some type checking to arguments for sanity's sake

credits: @ThatLing for reviewing my code to ensure everything is tip top shape and can be read

meepen avatar May 08 '18 08:05 meepen

Should've used sorted array instead of linked list. This is not ideal.

thegrb93 avatar May 08 '18 10:05 thegrb93

sorted arrays and linked lists have no performance differences in lua.

meepen avatar May 08 '18 13:05 meepen

] - local T,t,nop,i,s,this={{}},SysTime,function(e)end,1 for i=1,1e7 do T[i]={{}}end this=T[i]s=t()::s:: nop(this[1]) i=i+1 this=T[i] if this then goto s end print(t()-s)
0.18326508712698
] - local this,t,nop,i,s,n={},SysTime,function(e)end,1 n=this for i=1,1e7 do n[1]={}n=n[1]end s=t()::s:: nop(this[1]) this=this[1] if this then goto s end print(t()-s)
0.13612537420715

meepen avatar May 08 '18 14:05 meepen

Why are you iterating a sequential array with 'goto'? No shit it'll be slower that way.

Goto for loop:

> local T,t,nop,i,s,this={{}},SysTime,function(e)end,1 for i=1,1e7 do T[i]={{}}end this=T[i]s=t()::s:: nop(this[1]) i=i+1 this=T[i] if this then goto s end print(t()-s)...
0.14187382361831

Goto Linked list:

local this,t,nop,i,s,n={},SysTime,function(e)end,1 n=this for i=1,1e7 do n[1]={}n=n[1]end s=t()::s:: nop(this[1]) this=this[1] if this then goto s end print(t()-s)...
0.10879372726475

For loop:

> local T,t,nop,i,s,this={{}},SysTime,function(e)end,1 for i=1,1e7 do T[i]={{}} end s=t() for i=1, #T do this=T[i] nop(this[1]) end print(t()-s)...
0.054398570300123

thegrb93 avatar May 08 '18 15:05 thegrb93

I think the main problem is the branching in the linked list, after that it's fine. If I use an iterative loop as you are doing it would be fine. I will update it.

meepen avatar May 09 '18 02:05 meepen

local this,t,nop,i,s,n={},SysTime,function(e)end,1 n=this for i=1,1e7 do n[1]={false}n=n[1]end s=t()for i = 1, 1e7 do nop(this[1]) this=this[1] end print(t()-s)
0.041814087393362
local T,t,nop,i,s,this={{}},SysTime,function(e)end,1 for i=1,1e7 do T[i]={{}} end s=t() for i=1, #T do this=T[i] nop(this[1]) end print(t()-s)
0.0430561556039

meepen avatar May 09 '18 02:05 meepen

Please merge this, I'll pay with my liver!

mcNuggets1 avatar May 09 '18 03:05 mcNuggets1

@thegrb93 I remember why I didn't use arrays and that is because when removing something after IsValid fails will screw up ordering. Linked lists made it a ton simpler.

meepen avatar May 09 '18 04:05 meepen

It's a LOT faster now, and still works perfectly.

meepen avatar May 09 '18 04:05 meepen

It looks a lot better, just one more thing. The Add function can't modify the 'event_table' because it might happen while Call is iterating, causing skipping or multi-calling of hooks. It should modify (or create if it doesn't exist) a separate staging table which 'Call' will check for and update to before iterating. Of course hook.Call can be called from inside a hook as well so I think a counting sentinel variable is needed to keep track of how many hook.Call is in the stack, and only update if it's the first one. Idk, maybe you have a better solution.

This would allow the Remove function to remove the hook instead of setting it to noop as well.

thegrb93 avatar May 09 '18 07:05 thegrb93

Tested the original vs https://github.com/Facepunch/garrysmod/pull/1485/commits/59b0b95347bbbdfa302b7ab0da9f0f12a4e1e269 vs https://github.com/Facepunch/garrysmod/pull/1485/commits/be3ec1c9c60e100f3adfe2652d9569dd8a4f4386

with

local a = SysTime()
for i=1, 1000 do
	hook.Add("TestHooks",tostring(i),function() end)
end
a = SysTime() - a
local b = SysTime()
for i=1, 100000 do
	hook.Call("TestHooks")
end
b = SysTime() - b
local c = SysTime()
for i=1, 1000 do
	hook.Remove("TestHooks",tostring(i),function() end)
end
c = SysTime() - c
print("hook.Add x 1000: "..a.."\nhook.Call x 100000: "..b.."\nhook.Remove x 1000: "..c)

original

] lua_openscript testhookcall.lua
Running script testhookcall.lua...
hook.Add x 1000: 0.00068461756018223
hook.Call x 100000: 3.7898931138393
hook.Remove x 1000: 0.00061366894578896
] lua_openscript testhookcall.lua
Running script testhookcall.lua...
hook.Add x 1000: 0.0006204956162037
hook.Call x 100000: 3.7789762919474
hook.Remove x 1000: 0.00058343654609416
] lua_openscript testhookcall.lua
Running script testhookcall.lua...
hook.Add x 1000: 0.00062707847746424
hook.Call x 100000: 3.7821104652867
hook.Remove x 1000: 0.00057977940105047

https://github.com/Facepunch/garrysmod/pull/1485/commits/59b0b95347bbbdfa302b7ab0da9f0f12a4e1e269

] lua_openscript testhookcall.lua
Running script testhookcall.lua...
hook.Add x 1000: 0.0050909897697125
hook.Call x 100000: 1.7501787124899
hook.Remove x 1000: 0.0048793629740658
] lua_openscript testhookcall.lua
Running script testhookcall.lua...
hook.Add x 1000: 0.0048444981909483
hook.Call x 100000: 1.6998171427457
hook.Remove x 1000: 0.0048374277104131
] lua_openscript testhookcall.lua
Running script testhookcall.lua...
hook.Add x 1000: 0.0049232487151016
hook.Call x 100000: 1.7172124752533
hook.Remove x 1000: 0.0045911799413147

https://github.com/Facepunch/garrysmod/pull/1485/commits/be3ec1c9c60e100f3adfe2652d9569dd8a4f4386

] lua_openscript testhookcall.lua
Running script testhookcall.lua...
hook.Add x 1000: 0.15113273973793
hook.Call x 100000: 1.7683649635746
hook.Remove x 1000: 0.0013819132232129
] lua_openscript testhookcall.lua
Running script testhookcall.lua...
hook.Add x 1000: 0.15183661826222
hook.Call x 100000: 1.771345049201
hook.Remove x 1000: 0.0013687475011466
] lua_openscript testhookcall.lua
Running script testhookcall.lua...
hook.Add x 1000: 0.15025258682044
hook.Call x 100000: 1.7688340533846
hook.Remove x 1000: 0.0013592389236692

I'm guessing the queue is adding some overhead. hook.Add is quite a bit slower than linked list version. Both hook.Call are considerably better than original though.

thegrb93 avatar May 09 '18 16:05 thegrb93

Add and Remove performance doesn't matter since it's not called 5 bajillion times a second.

meepen avatar May 09 '18 17:05 meepen

I think I know one major improvement of this implementation. Currently the order hooks are called is does not correspond to the order they were added in. In your implementation this is also a problem since hooks of the same priority will also get called in any order. I don't know if it's feasible to add this without sacrificing a lot of performance, but if it is you should go for it.

I think you should also really change these comments: [ --[[comment]] ] and rather have them be above the line or at the end of the line, because currently the code is really annoying to read.

FredyH avatar May 11 '18 21:05 FredyH

I think I could make it so order of hook adding matters.

Also, the comments like that are for easy replacing if it ever gets changed. I probably can just add locals to the top of the file though.

meepen avatar May 12 '18 11:05 meepen

I've been testing https://github.com/Facepunch/garrysmod/pull/1485/commits/be3ec1c9c60e100f3adfe2652d9569dd8a4f4386 for a while and found a case where hook.Add wasn't working. I'll update to latest and see if its fixed.

thegrb93 avatar May 13 '18 22:05 thegrb93

If you add a remove a hook from inside its call and try re-adding it, the add will fail

hook.Add("test","blah",function()
    hook.Remove("test","blah")
    hook.Add("test","blah",function() end)
end)

thegrb93 avatar May 13 '18 23:05 thegrb93

Tomorrow this is opened for 1 month. Can we get any reply from the devs or the author about "thegrb93's" comment about this?

mcNuggets1 avatar Jun 07 '18 14:06 mcNuggets1

Hey, I'm working on this again. Currently optimizing and the fix for grb's situation was just changing the direction the update list was iterated.

meepen avatar Jun 17 '18 06:06 meepen

I change my mind, I'm going back to linked lists.

BENCHMARK
-------------
CallInvalid (200 calls)
hooklib/oldhook.lua (-357.29%)
includes/modules/hook.lua: 0.010206976 s
hooklib/oldhook.lua:       0.002232051 s
-------------
CallNoHooks (200000000 calls)
includes/modules/hook.lua (-3.44%)
includes/modules/hook.lua: 0.056299842 s
hooklib/oldhook.lua:       0.058236223 s
-------------
CallGMOnly (200000000 calls)
includes/modules/hook.lua (-3.85%)
includes/modules/hook.lua: 0.056151497 s
hooklib/oldhook.lua:       0.058313454 s
-------------
CallNoGM (32000000 calls)
includes/modules/hook.lua (-75642.34%)
includes/modules/hook.lua: 0.009049022 s
hooklib/oldhook.lua:       6.853941213 s
-------------
CallGM (32000000 calls)
includes/modules/hook.lua (-71738.51%)
includes/modules/hook.lua: 0.010311990 s
hooklib/oldhook.lua:       7.407980329 s

This benchmark was done with 3 hooks in each Call* that had hooks. CallInvalid added 25 invalid and valid hooks each time, so the performance will probably be messed up from the Add calls.

The problem with the old version(s) was that they had issues or were unoptimized or had code that was blacklisted by the jit.

I will be updating the main post soon.

meepen avatar Jun 17 '18 15:06 meepen

ULib goes from lowest to highest, priority wise, this is the opposite. Thus it will break ULib compatibility.

mcNuggets1 avatar Jun 11 '19 20:06 mcNuggets1

ULib would still override the hook lib with their version.

Kefta avatar Jun 12 '19 00:06 Kefta

Yeah, but I guess if they want to remove their hook replacement, they have to do more work in order to do so. Also it breaks a lot of addons ulib dependant.

mcNuggets1 avatar Jun 12 '19 00:06 mcNuggets1

They can just reverse the priority values for their replacement since it uses enums and not raw numbers.

Kefta avatar Jun 12 '19 01:06 Kefta

Yeah, works for me. Even tho a lot of ulib hooks are gonna break possibly.

mcNuggets1 avatar Jun 12 '19 02:06 mcNuggets1