Optimizing Plugins (AMX Mod X Scripting)

From AlliedModders Wiki
Revision as of 20:37, 17 January 2007 by Qqqqqqq (talk | contribs) (Compiler Optimizations)
Jump to: navigation, search

Introduction

Admin-Mod and AMX Mod X became very popular because of their easy to use scripting language. However, the words "scripting language" come with a lot of loaded preconceptions. Most people assume that because it's scripted:

  • You can't possibly make it any faster
  • It's pre-compiled, so it's already quite fast
  • Details don't matter, as it's only "scripting" anyway

Especially, with Pawn (formerly Small), none of these are true. The compiler, in fact, is very poor at optimizing, and you can greatly increase the speed and efficiency of your plugins by keeping a few rules in mind. Remember - it's more important to minimize instructions than it is to minimize lines of code.

Terms

To read this document, you will need to understand a few concepts beforehand:

  • BRANCHING - When the processor takes a different path of code. For example, to call a function or to use an if statement, the processor will "branch". Modern processors attempt to predict pathways with "branch prediction", but it's best to avoid branching a lot if possible.
  • STACK ALLOCATION - In Pawn, all local data is stored on the stack, a big chunk of continuous memory. Whenever you create a variable on the stack, it is automatically written with zeroes.
  • HEAP ALLOCATION - In Pawn, temporary data that needs to be referenced by a native is stored on the heap, another area of contiguous, but less restrictive memory.
  • DATA SECTION - This is an area of memory built into your .amxx file. In fact, it "becomes" the heap at load time. All your strings and arrays are hardcoded into this area.
  • EXPENSIVENESS - To be "expensive" in computer science means an operation requires a lot of CPU processing. It usually does not refer to memory size, only to processing cycles and time. Addition is inexpensive, floating power operations are expensive. However, both are inexpensive in comparison to writing a file. An inexpensive operation can also be called "cheap".
  • BIG-OH NOTATION - O(*) notation refers to the expensiveness of an algorithm. If something is O(n), it occurs in linear time -- meaning that for N items, it will complete relative to N. O(N^2) means with N items, it will complete relative to N^2. O(1) means "constant time" - no matter what N is, it will run in the same amount of time.

LOL HAX

Tips and Tricks

Lookup Tables

Precompute what can be precomputed. For example, say you have a mapping of weapon indices to names:

if (weapon == CSW_AK47)
   copy(name, len, "weapon_ak47")

Ignoring the fact that we have get_weapon_name in AMX Mod X, this is inefficient. We could precompute this result in a table:

new g_WeaponNamesTable[TOTAL_WEAPONS][] = {
   //..0 to CSW_AK47-1
   "weapon_ak47",
   //..CSW_AK47+1 to TOTAL_WEAPONS-1
};

Perfect Hashing

TODO: explain this


Local Strings

The Pawn compiler does not optimize the DATA section, which stores all hardcoded strings and global arrays. If you reference the same hardcoded string 500 times in your plugin, it will appear 500 different times. If this seems bad enough, it actually does this with all strings. For example, the empty string ("") appears everywhere in the include files, usually used as a default parameter to many natives. This too is copied into the data section for each unique reference.

For example:

set_cvar_string("amx_gaben", get_cvar_string("amx_gaben") + 1)

This will create two copies of "amx_gaben" in the DATA section. While this doesn't really hurt, it does increase the size of your plugin.

Similarly, this has the same problem:

#define AMX_GABEN "amx_gaben"
set_cvar_string(AMX_GABEN, get_cvar_string(AMX_GABEN) + 1)

The only way to avoid this mess is to use global variables. As stated earlier, they're basically free storage.

new AMX_GABEN[] = "amx_gaben"
set_cvar_string(AMX_GABEN, get_cvar_string(AMX_GABEN) + 1)

Again, while not necessary, this will reduce your plugin's size in memory and on disk. If you're already using defines, you can make this switch easily.

In order to prevent this from changing, you may want to declare it constant:

new const AMX_GABEN[] = "amx_gaben"
set_cvar_string(AMX_GABEN, get_cvar_string(AMX_GABEN) + 1)

Now it is a perfectly safe method of storage.

Faster Natives

AMX Mod X replaces many of the old AMX Mod natives with faster versions. Read below to discover them.

Cvar Pointers

As of AMX Mod X 1.70, you can cache "cvar pointers". These are direct accesses to cvars, rather than named access. This is a critical optimization which is dozens of times faster. For example:

new g_enabled = register_cvar("csdm_enabled", "1")
//OR
new g_enabled = get_cvar_pointer("csdm_enabled")
 
stock SetCSDM(num)
   set_pcvar_num(g_enabled, num)
 
stock GetCSDM()
   return get_pcvar_num(g_enabled)

All of the cvar* functions (except for set_cvar_string) are mapped to [get|set]_pcvar_*. You can get a cached cvar pointer with get_cvar_pointer() or register_cvar().

FormatEX

As of AMX Mod X 1.70, there is an ultra high-speed version of format() called formatex(). It skips copy-back checking, unlike format(). formatex() cannot be used if a source input is the same as the output buffer. For example, these are invalid:

new buffer[255]
formatex(buffer, 254, "%s", buffer);
formatex(buffer, 254, buffer);
formatex(buffer, 254, "%d %s", buffer[2]);

It should be noted that format() will behave the same as formatex() if it detects that there will be no copy-back needed. However, formatex() does not check this, and thus is slightly faster for situations where the coder is sure of its usage.

File Writing

As of AMX Mod X 1.70, there are new natives for file writing. Read_file and write_file are O(n^2) functions for consecutive read/writes. fopen(), fgets(), fputs(), and fclose() are O(n) (or better) depending on how you use them.