Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Edited DarkRP.findPlayers to cycle from 1 to # #3250

Merged
merged 3 commits into from
Mar 16, 2024
Merged

Conversation

IP1A
Copy link
Contributor

@IP1A IP1A commented Mar 6, 2024

No description provided.

@@ -97,23 +97,25 @@ function DarkRP.findPlayers(info)
local InfoPlayers = {}
for A in string.gmatch(info .. ";", "([a-zA-Z0-9:_.]*)[;(,%s)%c]") do
if A ~= "" then
table.insert(InfoPlayers, A)
InfoPlayers[#InfoPlayers+1] = A
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you really want to optimise this, create an InfoPlayersCount variable and increment is inside the loop after each insertion so that the length doesn't have to be refetched on each iteration.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is table.insert really that slow that we should be avoiding it and doing this instead? At some point I prefer just using table.insert over doing manual accounting and accept the tiny cost it brings.

Copy link
Contributor

@Kefta Kefta Mar 10, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

See https://gitspartv.github.io/LuaJIT-Benchmarks/#test12 - this is a bit different of a case in application from the other non-tinsert examples since the local count will be reused for a later loop, but table.insert is still the slowest when the code is jitted. I don't think the readability is sacrificed by not using table.insert personally so long as the variables are well-named and there are no regressions which can be verified with a simple unit test of a function like this.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Look at the code though. This code splits the input string on semicolons and iterates over each element. I'm quite sure that this regex based split would already make the cost of table.insert completely invisible. On top of that, use of this semicolons syntax is extremely rare, so for most calls by far this is going to be a single iteration loop.

In DarkRP this function is only called for some chat commands. That's extremely rare performance wise. Let's not go to extreme lengths trying to optimize this function. It can only introduce bugs, make it less readable and for absolutely zero real world visible gain in performance.

end
end

for _, PlayerInfo in ipairs(InfoPlayers) do
for _ = 1, #InfoPlayers do
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Change _ to i. _ implies it's totally unused.

end
end

for _, PlayerInfo in ipairs(InfoPlayers) do
for _ = 1, #InfoPlayers do
local PlayerInfo = InfoPlayers[_]
-- Playerinfo is always to be treated as UserID when it's a number
-- otherwise people with numbers in their names could get confused with UserID's of other players
if tonumber(PlayerInfo) then
if IsValid(Player(PlayerInfo)) and not found[Player(PlayerInfo)] then
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While you're here, cache the return of Player(PlayerInfo) instead of calling it 4 times per iteration.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a good idea 👍

@Kefta
Copy link
Contributor

Kefta commented Mar 6, 2024

I would also rename the PR to something that makes it clear this is an optimisation and not functionality changing.

Copy link
Owner

@FPtje FPtje left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the optimizations!

Some of those changes might have a tiny tiny improvement in performance, but they also make things less readable: using ipairs and table.insert is nicer to read than manipulating tables manually. Since in this case I prefer readability, I reverted those particular changes.

@Kefta had a good idea about reducing the amount of calls to Player(). I've added some other optimizations as well.

The trick to optimizing functions like this is measurement. Grab your lua_run command, and use SysTime() before and after to measure the time for that function to do its work. Do that a couple of times to get an idea of its variation. Then go out to optimize, and keep measuring to see what the difference makes.

You can even temporarily put SysTime() calls in the code, to see which parts are the most expensive. That way you can hone in on the specific bottleneck.

end
end

for _, PlayerInfo in ipairs(InfoPlayers) do
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question with ipairs. The use of ipairs over pairs already is because it's faster

Copy link
Owner

@FPtje FPtje left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some notes on the changes I made

end
continue
end

local stringPlayerInfo = string.lower(tostring(PlayerInfo))
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This data was being converted to lowercase twice for every player in the loop below. If you have 128 players (with no found player), it would do this string conversion 128 times! Take it out of the loop, and it's done only once for every player.

if found[v] then continue end
local steamId = v:SteamID()
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SteamID was called twice per iteration, changing it to once and storing in a variable ain't much, but it also looks a bit nicer in the already long if-statement below.

@Kefta
Copy link
Contributor

Kefta commented Mar 10, 2024

The trick to optimizing functions like this is measurement. Grab your lua_run command, and use SysTime() before and after to measure the time for that function to do its work. Do that a couple of times to get an idea of its variation. Then go out to optimize, and keep measuring to see what the difference makes.

This is often unreliable since measuring can actually affect whether jitting happens or not. It's better to measure idioms in a LuaJIT interpreter and see what uses the least instructions/takes the least time in a normal benchmark, or if done in gmod, what the timing is from an external module which won't invoke Lua->C/C->Lua calls during the code execution itself. SysTime benchmarking is a great tool for the glua api where there are a lot of C funcs where stitching won't help, but for pure Lua idioms, it's best to do that outside of glua. The LuaJIT version per-branch is known so its efficient practices can be known without gmod.

@FPtje
Copy link
Owner

FPtje commented Mar 11, 2024

It's better to measure idioms in a LuaJIT interpreter and see what uses the least instructions/takes the least time in a normal benchmark, or if done in gmod, what the timing is from an external module which won't invoke Lua->C/C->Lua calls during the code execution itself.

That is better when you're trying to find out which Lua language construct will get you that last drop of performance in a vacuum chamber experiment. That's not what we're doing here, though. Here there is a real function, with C calls, with rather complex behavior, and with some reasoning about its call sites and most common input.

Without measuring, your best recourse is eyeballing and meme based optimizing: changing the things in code that you recognize from your tribal knowledge of "don't do this, do this instead". Without measuring, you might miss what the real bottleneck is in the code.

I'm happy with the contribution, but I dread the discussions on those micro optimizations. Did anyone even establish that this function causes performance problems? I'm sure you can prove a performance increase when you call it a million times, but it's called once per chat command in DarkRP. What performance increase are real people going to notice?

@FPtje
Copy link
Owner

FPtje commented Mar 16, 2024

One tiny more change to remove a tostring, because we can already be sure PlayerInfo is a string at that point of the code.

This PR is good to go. Thanks for the optimization!

@FPtje FPtje merged commit ccc8871 into FPtje:master Mar 16, 2024
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants