System resources consist typically of two things when you are scripting: memory space, and cpu cycles. There is almost always a trade-off between the two. Less cpu cycles leads to more memory use and vice-versa, within limits. Also you are often concerned with readability because it has much to do with maintainability (ease of change).
Second script invokes the function
GetEnteringObject three times to compute the exact same thing every time. So the first script is more efficient in this regard because it calls the function once to compute it, then stores the result in a variable for use throughout the script wherever it is needed. So that it need not ever be computed again. The variable also adds readability to the script because, presumably, you know from its name and datatype what it stores. However, that script uses more memory since it must reserve (i.e. allocate from the system's pool of unused memory) a space to hold and remember the result of the function call. Memory allocation is not a zero cpu operation. Neither is storing something in it or retrieving something from it. It is significantly faster than recomputing things in virtually every situation however. So there is a balancing act in play in this example, as there is in most.
The determining factor here, I would say, is the frequency that the function is called. I don't imagine that
GetEnteringObject is a particulary complex function in its implementation, although it being a core function I cannot be 100% sure when scripting. Still my expectation is that it runs to completion extreemly quickly, and practice betrays this to be true. It computes its result almost as fast as a simple memory read from a saved variable. There is some overhead in calling the function and communicating its parameters and result, but the code it runs when you do, in order to compute your request, is not significant. Calling it superfulously 2 times is no massive problem. Still, if the information was available in a variable, it would be several times cheaper to read it from there than to call the function to compute it again. Measurably cheaper, but not noticably cheaper in this case. If a memory read takes, say, 2 milliseconds, then several times that would be on the order of what? 20 milliseconds at worst maybe? Measurable, but not noticeable.
It could add up if the script was called very very often and then possibly become significant enough to be noticeable. So the question of efficiency becomes "How frequently must the script run before it becomes noticeable?" Something you can only answer by experimenting. I am guessing you will never be able to work up an experiment that runs the script frequently enough that the difference will become noticeable. Not in this particular case. Because
GetEnteringObject is such an efficient core function. The difference between calling it and reading from a variable is awfully small. The gain is so small that the frequency would need to be higher than the game engine can produce. That's all just a guess though. But, now consider how different things would be if it was a custom function that was 500 lines long being called too often instead!
What catches my eye instead is this following bit, which is horribly wasteful (although again not noticeable), simply because it is unnecessary waste which accomplishes nothing:
[quote]
...
if( doOnce == TRUE ) return;
...
[/quote]
This is an inefficiency shared by both scripts. They are both making the double blunder of wasting both memory and cpu cycles in order to make this decision only once in the script. First off is the waste of memory. And remember, when you waste memory you are also forced into wasting cpu cycles because memory allocation, access, and release requires the cpu to be involved. If you did not need the memory, then you also did not need to waste the cpu cycles obtaining it, reading and writing to it, and finally giving it back.
In both these scripts, there is no need to save the result of the
GetLocalInt function into a variable since it is only ever used in one place in the whole script. You don't need to remember it later in order to avoid calling a function to recompute it over and over again because it is only ever needed once in one place. So the
doOnce variable is a waste of memory space and cpu time allocating space for it and later releasing that space when the script is done. All to remember something that will be used in the very next line of code and nowhere else. This waste can be resolved by eliminating the variable and calling the function directly in the if-statement's conditional clause...
[quote]
void main()
{ object oPC = GetEnteringObject();
if( GetLocalInt( oPC, GetTag( OBJECT_SELF )) == TRUE ) return;
...
[/quote]
That's better memory-wise for sure. Although it is harder to read...isn't as clear what is being tested. If readability was important, I would recommend wasting the memory in this case because the effect is not noticeable, yet the improvement in readability is great. If however efficiency was critical, I would abandon the variable. I will also take an opportunity here to mention that readability can be greatly improved by using a wrapper function. Wrappers are cpu users, but they are very cheap. If you are trying to conserve memory, like in this case, you can get your lost readability back inexpensively by wrapping the
GetLocalInt function in a wrapper with a name that makes sense. Say,
HasDoneThisBefore for example. So you get this:
[quote]
int HasDoneThisBefore( object oPC )
{ return GetLocalInt( oPC, GetTag( OBJECT_SELF ));
}
void main()
{ object oPC = GetEnteringObject();
if( HasDoneThisBefore( oPC ) == TRUE ) return;
...
[/quote]
This is far more readable, doesn't waste the memory on a
doOnce variable, but is minutely more costly cpu-wise than the more unreadable alternative. Because this DoOnce operation/decision is very common in NWN, the usefulness of creating such a wrapper and throwing it into a library which you can use throughout your module far outweighs the tiny cpu hit you will get from calling it. Because you will be using it to save on wasted memory, along with its inherent cpu use, you will probably break close to even cpu-wise anyway by doing it. Thus the net result is saved memory...little to no change in cpu usage...perhaps even an improvement in readability. A win because you get to avoid, mostly, the typical trade off (memory vs cpu).
Now let's talk about the cpu waste. It involves testing anything against
TRUE. It is almost always pointless to do it. In this case, a variable, which is a boolean value (i.e. true/false, yes/no, etc.) drives the decision making. What is stored there is a value
TRUE or
FALSE. The if-statement's conditional clause, the stuff between the parens, must always evaluate at run-time to a boolean value. It must represent a formula which computes down to a true or false value at run-time. The function
GetLocalInt, being used on a variable which is only ever given the value
TRUE or
FALSE, will therefore, on its own, just by viritue of calling it while knowing it will return a boolean value, resolve to the boolean value required by the if-statement at run-time.
Now if you have any boolean value, and you compare to see if it equals the value
TRUE, you are creating a boolean expression. The value of the expression, the equality test, will be used to drive the decision made by the if-statement. But by testing against
TRUE, you will find that the value of the resolved expression will always be the same as the value of the thing you are testing for truth. So if you ask "Is
X ==
TRUE?", where
X is known to be a boolean value, you will see that the answer to that question will always match the value of
X. If
X is set to the value of
TRUE, then the expression will be computed to mean
TRUE at run-time. If
X is set to
FALSE, the expression will likewise become
FALSE at run-time. So why waste the cpu time making the comparison?
X already represents the answer you're looking for!
It is very similar logically to multiplying or dividing by 1, adding or subtracting by 0. Here's an example to make it even clearer. Which of these statements would you expect to use more cpu?
[quote]
int X = 7;
int Y = 7 * 1;
int Z = (3 * 1) + (4 * 1) + 0;
[/quote]
The second one has to perform a multiplication before it knows what value to put at memory location
Y, the first one does not. Yet they do exactly the same thing. You would never write a line of code that looks like that second one because it obviously makes no sense to waste cpu like that. And the third one is nothing short of absurd. It's the same thing when, in the boolean world, you say ==
TRUE. By writing it explicitly in there, you are forcing the script to perform a useless test, using cpu cycles, in order to compute a value you already know. There is no reason to ever ask "Is
X ==
TRUE" when you can ask "Is
X" and obtain the same answer.
Combining this revelation with the wrapper notion used to save the memory and improve readability/maintainability, it becomes quite obvious what the best way to go with in this case in order to save memory, minimize cpu, and maintain readability, which are the ultimate goals of scripting 'efficiently':
1. Make a library script for the code which accomplishes commonly needed tasks.
[quote]
// My Library script named mylib.nss
//
// int HasDoneThisBefore( object oPC )
// Determines if a given PC has interacted with OBJECT_SELF before.
int HasDoneThisBefore( object oPC );
int HasDoneThisBefore( object oPC )
{ return GetLocalInt( oPC, GetTag( OBJECT_SELF ));
}
// void SayThisOnce( object oPC, string sObservation )
// Causes a given PC to make an observation verbally unless he has done it
// already once before by interacting with OBJECT_SELF some time earlier.
void SayThisOnce( object oPC, string sObservation );
void SayThisOnce( object oPC, string sObservation )
{ if( GetLocalInt( oPC, GetTag( OBJECT_SELF )) || (sObservation == "") ) return;
SetLocalInt( oPC, GetTag( OBJECT_SELF ), TRUE );
AssignCommand( oPC, ActionSpeakString( sObservation ));
}
[/quote]
2. Use it in all your main scripts.
[quote]
#include "mylib"
void main()
{ SayThisOnce( GetEnteringObject(), "Looks like someone left the door open." );
}
[/quote]
Look how readable the main script has become!!! It practically screams "Make the entering creature say this but only once." And it does it in only one line instead of five. No
oPC variable in there cluttering things up anymore either. Notice also that neither library function is required to explicitly create a variable in order to do its job!!! That's all handled by function parameters now. Parameters require memory just like variables. You aren't saving any memory by using them like I have here. It's just handled automatically behind the scenes for you and the explicit reference which causes the allocation/release is in the function header instead of directly in the code all over the place. It would not surprise me if parameter memory was managed a tad more efficiently than explicit variables are since parameters are typically combined into a single block of memory for ease of handling in virtually every modern programming language. However, parameter memory is allocated and released every time you call the function while explicit locals are allocated and released only once each time the script is called. It makes sense when the code is commonly used to do it this way. If it isn't common, a library is a waste of time and effort. In that case add all your efficiency and readability directly in the script as best you can. Adding functions local within the main script can still be quite useful, in a like manner to this example, when the code is not common, especially to improve readability and therefore maintainability.
Notice I did not use
HasDoneThisBefore inside
SayThisOnce. I could have written it this way to be more readable:
[quote]
void SayThisOnce( object oPC, string sObservation )
{ if( HasDoneThisBefore( oPC ) || (sObservation == "") ) return;
SetLocalInt( oPC, GetTag( OBJECT_SELF ), TRUE );
AssignCommand( oPC, ActionSpeakString( sObservation ));
}
[/quote]
So why didn't I? Well, functions in libraries ought to be ultra efficient. As efficient as possible. Maximize efficiency in library functions at the expense of readability. The whole purpose of having a library is to centralize commonly needed code. Since it is common, it has a great impact on the whole of the resulting system. So efficiency is paramount over readability in a library. The names of functions and their parameters that the library provides is where the readability comes from. You don't gain the advantage until you use the library. The objective is to make all main scripts very efficient, readable, and simpler to write by transferring the more complex code into a centralized location where it can be made very efficient without concern for readability. Also by centralizing common things, you increase mantainability because when a change is called for, you need only do it in one place and the whole system gets changed. If the code is common but scattered about (copied) in 60 different scripts, then to change the entire system means changing all 60 places. And that requires remembering that there are 60 places to do it, as well as where all those 60 places are. And then, additionally, physically make the exact same change correctly 60 times in a row. I've never met a human who can do that. So by not calling
HasDoneThisBefore within the other library function, I am not wasting the overhead of a function call for the only reason to improve readability. It's a library and that's not what it is there for.
HasDoneThisBefore is simply included for use if ever some script needs it independently of
SayThisOnce.
Of course, in practice, most scripts use
oPC all over the place so you won't be able to get away with what I've done here with that variable every time. It will be more frequent that you will want an explicit local variable for that one, among others. You might also note that by making the
SayThisOnce code into a general purpose function, it 'required' (optionally) an additional test to ensure that should it be called with an empty string to speak, it would bail out and do nothing. This isn't absolutely necessary, and you could take it out if you didn't mind the function forcing a player say a blank line when told to do so, but it seemed like a reasonable safety check in this instance. And it illustrates also that overuse of functions can actually lead to widespread inefficiencies if you aren't careful. Testing for empty speech, which was never done before, wasn't necessary, is now being done everywhere at a cpu cost every time, because by making it into a general purpose function it suddenly became possible to have a blank statement to say. Another trade-off. Programming is always about trade-offs. Again, not even noticable in this example no matter what you choose. At least if you decide to change it though, you only need to do it in one place.
Maintainability is sometimes a bigger deal than efficiency is. And you usually trade a little efficiency to get it. If you're doing an online world it is an absolute necessity. A script that gets called infrequently but gets upgraded, morphed, and modified regularly needs to be readable more than it needs to be efficient. Concentrate your efficiency efforts on scripts that run frequently and never need changing once you get em working. Be most careful with scripts that run frequently and also need to be changed a lot. The end objective is to get the project completed. Tweaking things to make them more efficient but that nobody will ever notice does not get you there. Planning ahead using things like library functions is a much wiser use of time. They give you efficient customized tools, ready to use in every main script you tackle, that improve readability and therefore maintainability, so you need not worry nearly as much about efficiency when writing the main scripts. That allows you to concentrate on getting on with the project objectives when authoriing the main scripts, while still having confidence it will work right, remain reasonably efficient, and be easy to change when you're all done.
When planning ahead, or even when just starting on a new script, two great questions to ask yourself are "What things do I want to happen?" and "What information will I need to know in order to make this happen?". The answers will usually lead you to identify custom functions that you can create to gather that information efficiently or to cause something to happen (like an action sequence for example). And if it is commonly needed information, or a commonly needed sequence, those functions can be leveraged in a library for easy reuse throughout the module.
If you're just looking for the thinnest way to do this without regard for reuse or readability. I'd say this is probably it:
[quote]
void main()
{ object oPC = GetEnteringObject();
string sTag = GetTag( OBJECT_SELF );
if( GetLocalInt( oPC, sTag)) return;
SetLocalInt( oPC, sTag, TRUE );
AssignCommand( oPC, SpeakString( "They left door open." ));
}
[/quote]
Which brings up an interesting situation. The TAG of
OBJECT_SELF is determined by calling
GetTag twice in two different spots. However, depending on the variable's value the second call may or may not happen when the script is called because it will be bypassed by the if-statement sometimes. So, a decision to make. Do I put it into a variable to save having to call the
GetTag function twice, when that situation occurs, or do I leave in the multiple function calls to avoid using a variable I might not need? The answer is easy in this case. What condition do you expect to see most often when the script is called? Well, the first time the script is called the condition is
FALSE and it gets set to
TRUE. That's also when the
GetTag function gets called twice. Every other time the script is called from that point onward, the condition will be
TRUE and the second
GetTag call will be bypassed by the if-statement. So the best solution is to not concern yourself with the extra function call. Because it will only happen once per PC per
OBJECT_SELF. What I've done above is actually worse than calling the function twice because of how the script is expected to work during play. If the reverse had been true and you wanted him to speak his line every time except the first time, so the typical scenario would be to call the function twice, then the variable would be technically I think the best way to go. And similarly for the
oPC variable. Memory isn't as valuable as CPU most of the time today.
Therefore this is it:
[quote]
void main()
{ if( GetLocalInt( GetEnteringObject(), GetTag( OBJECT_SELF ))) return;
object oPC = GetEnteringObject();
SetLocalInt( oPC, GetTag( OBJECT_SELF ), TRUE );
AssignCommand( oPC, SpeakString( "They left door open." ));
}
[/quote]
Modifié par Axe_Murderer, 24 mai 2012 - 03:47 .