[Tutorial] Creative & smart scripting techniques - improve performance & readablity in one go
#1

Scripting Ideas
Improve readablity and performance at the same time.

These are some collection of ideas which improve performance significantly without compromising the readability of the code. Some of these ideas are shocking different and unexpected. This topic does not tell you tell you how to optimize your code by listing do's and dont's (we already have lot of topics for that) rather it gives some clever ideas using which you can improve the performance and readability at the same time. Before we dive into the details, I would like to stress on one golden rule again since this rule governs almost all kinds of optimizations that you can do including the ones mentioned in this thread.

The Golden Rule that every scripter needs to know is PAWN code is insanely slow compared to the natives!

The above fact has far-reaching consequences. If you write your own strcmp, the native strcmp will beat your version by a factor of at least five. Memcpy can set a complete array 50-100 times faster than using a plain PAWN loop. In other words, the rule just asks you to make use of native functions (could be a part of the standard library or could be a function provided by a plugin) whenever possible.

Every script that you create basically runs on a virtual computer called AMX Machine and this machine, in turn, runs on your real hardware. This poor little machine needs to first decode your PAWN code (it is actually P-Code/object code which the compiler generates from your PAWN code) then do a series of checks on every PAWN instruction after which it is executed on the real hardware. Now it's pretty much obvious that PAWN code is very slow.

The natives, on the other hand, are provided by the machine and hence are directly executed on your real computer which is why natives are faster.

Dummy Array & Dummy Array Element
The ideas mentioned here makes use of dummy arrays whose sole purpose is to store the default data so that we can use the memcpy native to copy the default data to an actual array instead of using a loop to initialize the array. This takes advantage of fastness of the memcpy function. In fact, all natives are tens of times faster in most cases when it comes to handling big chunks of data.

Initilizing large arrays (enum based)
This idea is best suitable for very big gamemodes where you have a lot of variables to be set in an array (enum based array). The idea is to have an extra element in your array which is initialized with the default data for the given array so that when you need the default data, one memcpy (which is amazingly fast compared to manual assignments) call would do.

Let us take an example. Assume that the following is enum array that you use.
Code:
enum playerInfo_t
{
      Name[MAX_PLAYER_NAME],
      MoneyInHand,
      BankMoney,
      Team,
      Skin,
      Kills,
      Deaths,
      WantedLevel,
      AdminLevel,
      Muted,
      Jailed,
      Frozen      
}
new PlayerInfo[MAX_PLAYERS + 1][playerInfo_t];
This is how you'd probably do which is really annoying since its takes lot of space in the code in such an important callback.

Code:
public OnPlayerConnect(playerid)
{
       PlayerInfo[playerid][Team] = -1;
       PlayerInfo[playerid][Skin] = 1;
       PlayerInfo[playerid][MoneyInHand] = 
       PlayerInfo[playerid][BankMoney] = 
       PlayerInfo[playerid][Kills] = 
       PlayerInfo[playerid][Deaths] =
       PlayerInfo[playerid][WantedLevel] = 
       PlayerInfo[playerid][AdminLevel] =  0;
       PlayerInfo[playerid][Muted] = true;
       PlayerInfo[playerid][Jailed] =
       PlayerInfo[playerid][Frozen] =  false;
}
If you were to use the idea mentioned in this section, you will have to make room for the dummy element in the array and initialize the dummy element in OnGameModeInit/OnFilterScriptInit.

Code:
public OnGameModeInit()
{
      PlayerInfo[MAX_PLAYERS][Team] = -1;
      PlayerInfo[MAX_PLAYERS][Skin] = 1;
      PlayerInfo[MAX_PLAYERS][MoneyInHand] = 
      PlayerInfo[MAX_PLAYERS][BankMoney] = 
      PlayerInfo[MAX_PLAYERS][Kills] = 
      PlayerInfo[MAX_PLAYERS][Deaths] =
      PlayerInfo[MAX_PLAYERS][WantedLevel] = 
      PlayerInfo[MAX_PLAYERS][AdminLevel] =  0;
      PlayerInfo[MAX_PLAYERS][Muted] = true;
      PlayerInfo[MAX_PLAYERS][Jailed] =
      PlayerInfo[MAX_PLAYERS][Frozen] =  false;
}
Now you have just one line in OnPlayerConnect and its super-efficient.
Code:
public OnPlayerConnect(playerid)
{
      memcpy(PlayerInfo[playerid], PlayerInfo[MAX_PLAYERS], 0, sizeof(PlayerInfo[])*4, sizeof(PlayerInfo[]));
      //or
      PlayerInfo[playerid] = PlayerInfo[MAX_PLAYERS];
}
Filling arrays
Suppose you have an array EventPoints[MAX_PLAYERS] and you wish to fill it with zeros before the start of an event. You might do the following,

Code:
for(new i = 0; i < MAX_PLAYERS; i++)
     EventPoints[i] = 0;
To be honest, this is an awful way to do it since it is pure PAWN code (PAWN code is insanely slow).

There are two efficient approaches (which are tens of times faster) to deal with cases where you want to fill a whole array with the same value.
  1. Using memset
  2. Dummy array
Using memset
Memset basically, as the name says, sets a region of memory with the same value. This is the most efficient way to fill an array with zeros if you don't want to make a dummy array. The C/C++ Standard memset sets a fixed number of bytes to a specific value but this is not the case with the PAWN version of memset (Slice's memset; PAWN library does not provide any memset). Slice's memset sets all the cells in the region of the memory to the fixed value. The PAWN Implementer Guide says it wrong about the FILL assembly instruction that it fills the bytes with a fixed value. It actually fills cells with a particular value.

You can obtain a copy of memset by Slice here.

That code itself is a source of inspiration since the code modifies itself on the fly. It is done so due to a limitation of the AMX assembly. The FILL instruction accepts a constant number as the operand (fixed during compile-time) and hence, a variable number of cells/bytes cannot be used. Slice smartly overcomes the problem by writing code that edits itself during run-time (Refer to AMX Assembly tutorial to learn how to write code that edits itself). The only other way left if not to use the self-editing code would be to have loops to separate the data into pieces of powers of 2 (why powers of 2? its a mathematical problem - you can express any number with sums of powers of two - binary number basically for those who understood) and fill piece by piece which would have affected the performance of memset. However, even such a memset would be faster than manually filling the array using a loop since it does not make use of arrays.

Code:
memset(EventPoints, sizeof(EventPoints), 0);
Using dummy array
This is even faster than memset but in my opinion, it doesn't really matter which method you use as long as you don't write plain PAWN code to fill the array (which is nowhere close to memset's speed).

Code:
new bigdummy_zero[1000];

memcpy(EventPoints, bigdummy_zero, 0, sizeof(EventPoints)*4, sizeof(EventPoints));
You can use the same dummy array to fill other arrays too.

Efficient handling of objects/areas/labels/
Most of you make use of loops to work with objects. These loops are relatively expensive since the checks you add inside the loop to find the some id from the objectid which contains arrays like in the example given below.

Code:
public OnObjectMoved(objectid)
{
      for(new i = 0; i < sizeof(Bombs); i++)
      {
           if(Bombs[i][bomb_objid] == objectid) 
           {
                   Bombs[i][Moved] = true;
                   break;
           }
      }
}
Here is a more efficient way to do the same but it takes some extra memory. I don't think few KBs of extra memory is of any concern even if you have limited amount of RAM. Moreover, the performance improvement will easily outweigh the cost of using extra memory especially when you have hundreds of those entities (areas, objects, etc).

The idea is to have arrays whose index is used as the object id. The array stores what type of object the objectid corresponds to. We first make an enum where the first entry is OBJECT_TYPE_NONE (this should be zero) and the every following entry in the enum takes up non-zero value.
Code:
enum
{
      OBJECT_TYPE_NONE = 0, //Keeping it zero is an advantage since the PAWN compiler initializes the data to zero by default
      OBJECT_TYPE_BOMBS = 1,
      OBJECT_TYPE_WEAPON,
      .
      .
      .
}
new ObjectsType[MAX_OBJECTS];
new ObjectsSpecialID[MAX_OBJECTS];
The first array gives you what type of object the objectid corresponds to and the second array gives you some additional information about the object. What you store in the ObjectsSpecialID array is up to you. Remember that you have the freedom to store different type of data for different type of objects, for example, you can use ObjectsSpecialID to store the bomb id (related to your script, say, Bombs[MAX_BOMBS][bomb_info]) and store playerid for cloths. They won't conflict because the ObjectsType array is first checked which distinguishes between a cloth and a bomb. The following example should clear all your doubts.

Code:
public OnObjectMoved(objectid)
{
      switch(ObjectsType[objectid])
      {
            case OBJECT_TYPE_BOMBS:
            {
                   new i = ObjectsSpecialID[objectid];
                   Bombs[i][Moved] = true;
            }
      }
}
This is extremely efficient when you have many objects since it avoids the loop and the dirty arrays which are involved in every iteration.

If you are using the streamer plugin, you will have to use two sets of arrays since streamer ids can sometimes conflict with a SAMP assigned id (you can have an id whose streamer id is 1 and you can have an object created using CreateObject to have an id as one too).

Code:
new DynamicObjectsType[MAX_OBJECTS];
new DynamicObjectsSpecialID[MAX_OBJECTS];

new NormalObjectsType[MAX_OBJECTS];
new NormalObjectsSpecialID[MAX_OBJECTS];
Of course, these are memory hungry but are definitely worth sacrificing some space.

String natives work on arrays too!
The idea is to store data in the form of strings so that string related functions can be used on the data. For example, if you would want to store if a player X shot another player Y before player Y dies, you would probably do something similar to the following,

Code:
enum pinfo
{
     bool:PlayersShot[MAX_PLAYERS]
}
new PlayerInfo[MAX_PLAYERS][pinfo];

public OnPlayerWeaponShot(...)
{
     PlayerInfo[hitid][PlayersShot][playerid] = true;
}
If you do so, you are going to have 'dirty expensive costly' loops when you'd want to search for players who had shot him. Let's say you wanted to reward the players who shot the player X after his death. You'd use something similar to the code given below

Code:
for(new i = 0, j = GetPlayerPoolSize(); i <= j; i++)
{
      if(PlayerInfo[playerid][PlayersShot][i]) { }
}
You are trying to access a 3D array which is very slow and that too inside a loop. You can do the same much faster (tens of times). But there could be a bit of compromise on the readability here for some of you. I make use of defines and give better names for the string natives and hence I find them more readable.

The idea is to convert the boolean array into a string (need to keep a null character at the end of the array) and store a character, say 'Y', if the player was shot by a player whose index is his shooter's playerid and 'N' if the player wasn't shot by that player.

Code:
enum pInfo
{
      PlayerShot[MAX_PLAYERS + 1], //+1 to make room for the null character
}
To obtain the players who shot, we use the strfind function.
Code:
new pos = 0;
while((pos = strfind(PlayerInfo[playerid][PlayerShot], "Y", false, pos)) != -1)
{
     //pos has the id of a player who shot
     pos++;
}
You can either use memset or a dummy to fill the array with 'N' (except the last character) when the player dies.

Code:
memset(PlayerInfo[playerid][PlayerShot], MAX_PLAYERS, 'N');
It is important that you use MAX_PLAYERS while filling the array instead of MAX_PLAYERS + 1. You should leave the last character a zero (its already zero so don't go to set it to 'N', just leave it as it is) so that strfind knows when it reaches the end or else strfind will start reading blocks of memory well past the array end which it shouldn't and you will see SAMP behaving weirdly which might ultimately end up in a crash.

You can similarly use other string natives while working with arrays provided that you have zero at the end of the array.
Reply
#2

Wow nice.

Ok so I want to ask a question, is the memcpy method to reset an array faster than this?
PHP Code:
enum P_DATA {
    
p_money,
    
p_stuff,
    
p_whatever
}
new 
    
Player[MAX_PLAYERS][P_DATA], 
    
resetPlayer[P_DATA]
    ;
    
// anywhere in the code:
    
Player[playerid] = resetPlayer
Reply
#3

Quote:
Originally Posted by PawnHunter
View Post
Wow nice.

Ok so I want to ask a question, is the memcpy method to reset an array faster than this?
PHP Code:
enum P_DATA {
    
p_money,
    
p_stuff,
    
p_whatever
}
new 
    
Player[MAX_PLAYERS][P_DATA], 
    
resetPlayer[P_DATA]
    ;
    
// anywhere in the code:
    
Player[playerid] = resetPlayer
You just described the dummy array.
Reply
#4

There is a difference. The method shown by PawnHunter will reset everything to 0 while Yashas' method can set any value. It is a very interesting way to reset an enum-array indeed!

As for smart techniques using streamer plugin is E_STREAMER_EXTRA_ID. When it loads all the houses for example, set that extra ID for the pickup/checkpoint to a value such as "MAX_HOUSES + index" and you can retrieve it back on pickup/entering cp and get directly the house the player is near, no unnecessary loops.
Reply
#5

I'm actually in love with the memcpy idea, thanks for this precious tutorial!
Reply
#6

Quote:
Originally Posted by Konstantinos
View Post
There is a difference. The method shown by PawnHunter will reset everything to 0 while Yashas' method can set any value. It is a very interesting way to reset an enum-array indeed!

As for smart techniques using streamer plugin is E_STREAMER_EXTRA_ID. When it loads all the houses for example, set that extra ID for the pickup/checkpoint to a value such as "MAX_HOUSES + index" and you can retrieve it back on pickup/entering cp and get directly the house the player is near, no unnecessary loops.
Well technically PawnHunter's way could have values,
resetPlayer[P_DATA]={0,-1,1 and so on}; while initializing, then Player[playerid] = resetPlayer;
Although I'm not sure which one is faster and if I remember correctly ****** did a little searching and PawnHunter's was faster in huge enum-arrays.

Rest points are quite interesting, I remember having argument about speed or memory, and with today's memory standard and sa-mp being single-threaded, speed wins everytime, having a couple megabytes of memory is no big deal with 16-32GB rams these days.
Reply
#7

Except not everyone runs their servers on super expensive dedis. You can run a very simple plain SA-MP server under Linux with just 128 MB of RAM. And while that should be plenty, it is wrong to assume that multiple gigabytes of RAM will be available.
Reply
#8

Well today we have all that memory, because its not 2004, where memory was limited
Reply
#9

Quote:
Originally Posted by Vince
View Post
Except not everyone runs their servers on super expensive dedis. You can run a very simple plain SA-MP server under Linux with just 128 MB of RAM. And while that should be plenty, it is wrong to assume that multiple gigabytes of RAM will be available.
Cheapest VPSes around have 0.5/1 GB of ram at least, and as you are well aware 4mB of data means 1,000,000 (might be of by a factor, I'm kinda too lazy to do the exact math ) data cells, so even if you try and sacrifice memory for a lot of things, you won't end up using more than 40 MB of ram anyhow, thats perfectly acceptable at least for me.
Reply
#10

PHP Code:
new pName[MAX_PLAYERS][MAX_PLAYER_NAME]; // global array
// OnPlayerConnect
GetPlayerName(playeridpName[playerid], MAX_PLAYER_NAME);
// Anywhere in the script
... pName[playerid
PHP Code:
// anywhere in the script
new pName[MAX_PLAYER_NAME];
GetPlayerName(playeridpNameMAX_PLAYER_NAME);
... 
pName 
which is more preferred first method or second? And why?
Reply
#11

Quote:
Originally Posted by PawnHunter
View Post
PHP Code:
new pName[MAX_PLAYERS][MAX_PLAYER_NAME]; // global array
// OnPlayerConnect
GetPlayerName(playeridpNameMAX_PLAYER_NAME);
// Anywhere in the script
... pName[playerid
PHP Code:
// anywhere in the script
new pName[MAX_PLAYER_NAME];
GetPlayerName(playeridpNameMAX_PLAYER_NAME);
... 
pName 
which is more preferred first method or second? And why?
I think the second one safer than other. I have experienced with global array sometimes names can be mixed other player's name. You should use that as stock.
Reply
#12

Quote:
Originally Posted by Freedom.
View Post
I think the second one safer than other. I have experienced with global array sometimes names can be mixed other player's name. You should use that as stock.
Stock is a keyword to tell compiler to ignore the 'function is unused' warnings, anyways I never experienced problems with global arrays at all, I was just asking.
Reply
#13

Quote:
Originally Posted by PawnHunter
View Post
PHP Code:
new pName[MAX_PLAYERS][MAX_PLAYER_NAME]; // global array
// OnPlayerConnect
GetPlayerName(playeridpNameMAX_PLAYER_NAME);
// Anywhere in the script
... pName[playerid
PHP Code:
// anywhere in the script
new pName[MAX_PLAYER_NAME];
GetPlayerName(playeridpNameMAX_PLAYER_NAME);
... 
pName 
which is more preferred first method or second? And why?
Thats again the argument of speed vs. memory, calling a variable or array is faster than calling a function, even a native one, so first one is faster, and second one uses less memory, then there is your personal reference of what path you choose to go.
Reply
#14

What's the equivalent code for this using memset or memcpy
Code:
new Var[MAX_PLAYERS][MAX_PLAYERS];
foreach(Player,i)
{
Var[i][playerid]=0;
}
I really cant figure out a way to do this
Reply
#15

Quote:
Originally Posted by DavidBilla
View Post
What's the equivalent code for this using memset or memcpy
Code:
new Var[MAX_PLAYERS][MAX_PLAYERS];
foreach(Player,i)
{
Var[i][playerid]=0;
}
I really cant figure out a way to do this
Dude, memset won't help that code. That is absolutely horrible code as is. Your array by default is 250000 cells, which is 1000000 bytes, which is just dumb to have in PAWN.

You are using y_iterate already, learn how to use it's more advanced iterators and you can make that code a lot better.
Reply
#16

Quote:
Originally Posted by PawnHunter
View Post
Wow nice.

Ok so I want to ask a question, is the memcpy method to reset an array faster than this?
PHP Code:
enum P_DATA {
    
p_money,
    
p_stuff,
    
p_whatever
}
new 
    
Player[MAX_PLAYERS][P_DATA], 
    
resetPlayer[P_DATA]
    ;
    
// anywhere in the code:
    
Player[playerid] = resetPlayer
Second one is FASTER for larger arrays whereas memcpy is faster for smaller arrays.

^ and the array size where they meet in performance depends on the CPU!

I did not mention anything about that in the tutorial is because the speed gain is insignificant.

If you do it with a normal loop, say it takes around 1000ms. If you do it with memcpy, you can do it in less than 20ms. If you do the direct array indexing, you may do it in 15ms.

Ask yourself, is the 5ms gain significant compared to the 980ms gain?
Deciding when to use memcpy and when to assign directly would be a unnecessary headache for such an insignificant improvement. Even if assigning directly was 2x faster than memcpy, it would be insignificant. Why? Check the numbers again!

The numbers are guesses but the actual measurements will yield the same results.
Reply
#17

LOL i was about to create that tutorial!
Reply
#18

Quote:
Originally Posted by Yashas
View Post
Second one is FASTER for larger arrays whereas memcpy is faster for smaller arrays.

^ and the array size where they meet in performance depends on the CPU!

I did not mention anything about that in the tutorial is because the speed gain is insignificant.

If you do it with a normal loop, say it takes around 1000ms. If you do it with memcpy, you can do it in less than 20ms. If you do the direct array indexing, you may do it in 15ms.

Ask yourself, is the 5ms gain significant compared to the 980ms gain?
Deciding when to use memcpy and when to assign directly would be a unnecessary headache for such an insignificant improvement. Even if assigning directly was 2x faster than memcpy, it would be insignificant. Why? Check the numbers again!

The numbers are guesses but the actual measurements will yield the same results.
What are you even talking about?

50,000 iterations:



Part 1 => http://pastebin.com/zULdXUm9
Part 2 => http://pastebin.com/A8WaE6fv
Part 3 => http://pastebin.com/gX0Yv2Fw
Part 4 => http://pastebin.com/95kJi2a6
Reply
#19

You misinterpreted literally everything in my reply. xD

I was comparing memcpy with "array1 = array2;".

Not with and without a loop. Obviously, in that case loop would be slower.

Let me make it more clear on the terms I used.

Normal Loop
Code:
for(new i = 0; i < 50000; i ++)
array[i] = 0;
Memcpy
Code:
memcpy(array, dummy, 0, 50000*4);
Direct assignment
Code:
array = dummy;
Reply
#20

Quote:
Originally Posted by Yashas
View Post
You misinterpreted literally everything in my reply. xD

I was comparing memcpy with "array1 = array2;".

Not with and without a loop. Obviously, in that case loop would be slower.

Let me make it more clear on the terms I used.

Normal Loop
Code:
for(new i = 0; i < 50000; i ++)
array[i] = 0;
Memcpy
Code:
memcpy(array, dummy, 0, 50000*4);
Direct assignment
Code:
array = dummy;
That's old data. But what are you trying to achieve here? Normally setting variables is already fast.

You are kinda of a freak with performance and all that, it isn't always about performance in a production server, it's also about functionality, which should be put first. Without functionality and good gameplay, performance is nothing.

It's good that you share your tips and all that, but it would be nice to see you do something else. As long as performance is decent or reasonable, then I don't see the problem in leaving it like that. Server owners should focus more on the functionality.

This is all for server owners anyway.
Reply


Forum Jump:


Users browsing this thread: 1 Guest(s)