25.06.2016, 16:16
(
Last edited by Yashas; 31/01/2018 at 06:00 PM.
)
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.
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.
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.
Now you have just one line in OnPlayerConnect and its super-efficient.
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,
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.
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.
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).
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.
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.
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.
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).
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,
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
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.
To obtain the players who shot, we use the strfind function.
You can either use memset or a dummy to fill the array with 'N' (except the last character) when the player dies.
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.
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];
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; }
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; }
Code:
public OnPlayerConnect(playerid) { memcpy(PlayerInfo[playerid], PlayerInfo[MAX_PLAYERS], 0, sizeof(PlayerInfo[])*4, sizeof(PlayerInfo[])); //or PlayerInfo[playerid] = PlayerInfo[MAX_PLAYERS]; }
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;
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.
- Using memset
- Dummy array
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);
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));
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; } } }
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];
Code:
public OnObjectMoved(objectid) { switch(ObjectsType[objectid]) { case OBJECT_TYPE_BOMBS: { new i = ObjectsSpecialID[objectid]; Bombs[i][Moved] = true; } } }
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];
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; }
Code:
for(new i = 0, j = GetPlayerPoolSize(); i <= j; i++) { if(PlayerInfo[playerid][PlayersShot][i]) { } }
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 }
Code:
new pos = 0; while((pos = strfind(PlayerInfo[playerid][PlayerShot], "Y", false, pos)) != -1) { //pos has the id of a player who shot pos++; }
Code:
memset(PlayerInfo[playerid][PlayerShot], MAX_PLAYERS, 'N');
You can similarly use other string natives while working with arrays provided that you have zero at the end of the array.