Decompiling AMX Plugins, Part 5

I’m skipping order a bit to take a break. First, Wraith has released a shiny new version of AMXReader! This version is more modular and organized, supports new Small 3.0 opcodes, and can read AMX Mod X 1.60 plugins. Lastly, there’s a binary only download now.

Today I’d like to look back on what we’ve accomplished so far, and note some rules from places where optimization was obvious.

Rule #1: Only calculate something once!
We saw in sample.amx that

was called in each
case. We also saw in the disassembly that this added a good deal of extra instructions, not counting the hundreds of actual processor instructions that occur to make SYSREQ.C jump from AMX to the C native, and run the native, the actual native function itself, and then the return to caller. It is an expensive operation, and it could be possible that
itself is very expensive (although it’s not).

The rule of thumb is to calculate once and save the result, because memory lookup is ALWAYS faster:

new team = get_user_team(id)
if (team == 1) 
else if (team == 2)

Rule #2: Watch array limits!
Small itself checks array bounds for direct accesses, but natives don’t have to, and usually don’t even try. For example:

new str[]="60"
new array[32]
array[str_to_num(str)] = 5

Will generate an AMX_ERR_BOUNDS error. However, this will not:

new gaben[60]
new array[1]
copy(array, 60, "Hello!")

Obviously, I meant to copy to

, but instead I’ve copied into
. Remember the stack layout before? Each variable is laid one top of the other on the stack. The
function will simply copy 60 cells with no bounds checking, writing an ‘H’ into
, but filling
with the rest. This leads to all sorts of whacky errors and often crashes. The lesson is: always pass a buffer at least one less than the maximum size! An example of this problem in action is a mistakenly filed bug report from a user seeing a totally different variable being clipped by a string terminator.

Rule #3: If you’re going to repeat something, cache it.
This rule extends from #1. For example, we noticed that the DAT section is not optimized. You can repeat a string a 800 times and the DAT section will have an entry for each repetition. The solution?

#define COMMON_STRING   "Gaben"
new COMMON_STRING[] = "Gaben"

This way, DAT will only have one reference. Furthermore, it might even be faster because in the case of non-const the compiler won’t have to copy it into the heap first. In many cases you know it will never be modified, and the native declaration simply forgot a ‘const’ keyword.

Another example:

stock GetWeaponName(id, name[], max)
   if (id==0)
      copy(name, max, "weapon_none")
   else if (id==1)
      copy(name, max, "weapon_gaben")
new g_WeaponNames[] = {
     "weapon_none", //0
    "weapon_gaben", //1 

A lookup table is near-instantaneous execution. The compiler simply has to add an integer to a base memory offset. Calling a function is expensive — branch prediction, cache misses, more instructions can add up very quickly. If you’re using the function very often, it’s a good idea to take the time to make these trivial and worth-while optimizations. You can do lookup tables in Small for anything which inputs an integer and outputs any other data type, as long as the set of inputs has fixed limits. It is okay to build the lookup table at runtime, as long as you don’t need to do it often (or building it often outweighs the cost of computing entries totally dynamically).

4. You don’t need to make everything public.
Many people simply prepend “public” to every single function. You don’t need to do this. While it only adds a few bytes (it must store the name of the function), it’s bad design. Public means “external,” or “visible to everyone”. Private functions should not have this keyword. In the “sample.amx” example, “func_00″ was private, as it should have been.

5. You don’t need to return PLUGIN_CONTINUE and PLUGIN_HANDLED every time.
The compiler will automatically return 0 for you. However, because people tend to not obey #4, they also tend to randomly return either value at the end of every single function. This is not necessary. The rules for returning are as follows:
Explicit return – a return where you return a value (return X;)
Implicit return – a return where you specify no value (return;)
If you specify an explicit return, you must specificy an explicit return for each control path.
If you specify an implicit return, the compiler will return 0 for every control path.
If you specify no returns, the compiler will return 0 for every control path.

Lastly, the majority of public forwards do nothing with the return value. For example, returning an explicit value in client_disconnect, a set_task, or plugin_init will do absolutely nothing.

Leave a Reply

You must be logged in to post a comment.