Optimizing Plugins (SourceMod Scripting)

From AlliedModders Wiki
Revision as of 13:51, 26 March 2020 by Psychonic (talk | contribs) (pre -> sourcepawn)
Jump to: navigation, search

Introduction

This guide contains some general suggestions as to how to improve local performance in your code. However, take note. Do not use this as a guide to prematurely optimizing your program. You should focus on making your scripts easily readable and maintainable. Premature optimization can make your code more complex, introducing bugs that you otherwise wouldn't have had.

If you do notice performance problems on your server, and you think they are introduced by your plugin, there are a few steps you can take. The best way to start is to get a profiler. You can use the SourceMod Profiler to tell you how much time is being spent in script functions.

However, if you are worried about your code, you can also try to estimate the cost of operations in your program. Anything that happens repeatedly in a small period of time - the contents of a loop, the body of a timer, an OnGameFrame hook - are good targets.

DISCLAIMER: The units of time in this article are comparative only. We estimate most SourcePawn operations as costing 1-10 cycles, where a cycle is measured in nanoseconds.

Estimating Cost

The goal of estimating cost is to figure out two things:

  • How expensive an operation is, run only once.
  • How many times the operation is repeated.

Let's try this with a simple example loop below. Most syntactic language features have a simple cost. Natives can be trickier.

for (new i = 0; i < strlen(string); i++) {
    if (string[i] == '"')
        return i;
}

First, let's determine how many times this loop will run. If the length of string is n, then the loop will run n times. Next, let's see what each iteration of the loop costs:

  • strlen(string): This has to count all of the characters in |string|. Let's say counting a character costs 1 unit of time. Therefore, |strlen(string)| will cost n units of time.
  • if (string[i] == '"'): This contains an array load and comparison. Let's say those each cost 1 unit of time, totaling 2.
  • i++: This increments a local variable. Let's say that costs 1 unit of time.

Therefore, every iteration of the loop costs n + 3 units of time.

That means this loop may cost up to (n * (n + 3)) units of time. Now, if we know that n is always small - say, under 100 - that might not be a problem. But what happens if the string has 10,000 characters? Now, the loop will take over 100,000,000 units of time! If a "unit of time" is even as small as a nanosecond, that loop will take a whole tenth of a second, delaying the server by multiple frames!

This example is easy to fix. We can identify that strlen magnifies the cost of the loop, and rewrite it like this:

new length = strlen(string);
for (new i = 0; i < length; i++) {
    if (string[i] == '"')
        return i;
}

Now the cost of this loop is just: n times a very, very small amount of time.

Decl on Local Arrays

Here is another example, using OnGameFrame:

public OnGameFrame()
{
    new String:buffer[4096];
}

When you declare an array with new, Pawn initializes each slot to 0. Let's say writing to an array slot costs 1 unit of time. Declaring this array therefore costs a fixed 4096 units of time. Is that a problem? Well, the game server ticks at 33 times per second. Therefore this costs about 150,000 units of time per second. If a "unit of time" is a nanosecond, we could estimate this costing around 0.1 milliseconds. That could be a lot depending on what your plugin does. You get about 15ms per frame, of which about half is used by the server. If you use more than this time, you could start delaying frames.

Let's say that you have declared this big array inside a loop, inside OnGameFrame, and you are worried about the cost. The preferred way of solving this is to use decl instead of new, which does not perform any initialization:

public OnGameFrame()
{
    decl String:buffer[4096];
}

NOTE: You must be very careful not to read from uninitialized variables. They will contain garbage, and you could crash the server by attempting to print or operate on garbage strings.

You should only use decl on performance-critical arrays. It is never needed anywhere else.

Avoid Large KeyValues

KeyValues is an n-ary structure using linked lists. This type of structure is extremely expensive to allocate and traverse. While it might be suitable for tiny pieces of information (that is, under 10KB of data or so), its complexity growth is very poor.

If you load KeyValues data, you should make an effort to, at the very least, cache its Handle so you don't need to reparse the file every time. Caching its contents on a needed basis would be a bonus as well.

If you're trying to use a KeyValues file with thousands of entries and updating/loading it on events such as player connections or disconnections, you will find that the structure will grow to an unmanageably slow size. If that's the case, you should consider moving to something like SQLite or MySQL.