Most optimizations trade, at the very least, readabiliy for efficiency, and often it is additionally a tradeoff between speed and memory use.

I cannot stress enough that optimization starts at the design and algorithm level, not at the compiler quirks. If your algorithm is not well thought out, no amount of dirty tricks can beat a better written algorithm. Always focus on writing a good algorithm, rather than getting lost in using all the nifty tricks.

Also, do remember that by far the largest impact a script has is from causing updates in the world; not from crunching numbers in the bytecode. If your script is just reacting to a touch or a sitter now and then, there's little point in pulling out all the stops to optimize that. It's only when you're quickly and repeatedly spinning loops and calculating things that these optimizations will be relevant.

That said, there are a couple of pretty much no-lose optimizations, typically based on the fact that the LSL compiler does no optimizations at compile-time. Many modern compilers do detailed analysis of the code to do the following optimizations for you automatically, but the LSL compiler is not one of them.

Lift invariants out of loops

One very important technique is to lift expressions/calculations which do not change out of loops.
Do not do something like

for (i = 0; i < llGetListLength(myList); i++)

since it forces the code to constantly call the llGetListLength and re-evaluate the length. Instead, do it this way:

integer length = llGetListLength(myList);
for (i = 0; i < length; i++)

Use the correct type

When passing a value, use the correct type.

vector v = <1.0, 1.0, 1.0>;

is faster and uses less memory than

vector v = <1, 1, 1>;

since vectors consist of floats, and passing an integer forces the script to cast it to the appropriate type before it can be used. This is not done automatically at compile-time.

Don't calculate constants

While having something like

integer daysInSeconds = 60 * 60 * 24 * days;

is nicely readable, it is not calculated at compile-time, but rather every time the line execute in the running script. So:

integer daysInSeconds = 86400 * days;

will be faster and smaller.

Minimize function calls


changed(integer change)
    if (llAvatarOnSitTarget() != NULL_KEY)
        llRequestPermissions(llAvatarOnSitTarget(), PERMISSION_TRIGGER_ANIMATION);
//It would be good style to  include an if(change & CHANGED_LINK){…}, but I've omitted that for clarity.

The above works, and it is quite clear what it does. It is slightly suboptimal, though, since it calls the llAvatarOnSitTarget() twice, drawing upon the full functionality and calling overhead twice

changed(integer change)
    key sitter = llAvatarOnSitTarget();
    if (sitter != NULL_KEY)
        llRequestPermissions(sitter, PERMISSION_TRIGGER_ANIMATION);

…is considerably better, getting rid of all the calling overhead, storing the avatar key once and for all. The downside is that it clutters the code a little bit more, but the tradeoff is worth it.

But it can actually be optimized further:

Miminize stack popping

Now we're getting to the true tradeoffs, at least when it comes to readability:

changed(integer change)
    key sitter;
    if ((sitter = llGetAvatarOnSitTarget()) != NULL_KEY)
        llRequestPermissions(sitter, PERMISSION_TRIGGER_ANIMATION);

This is probably not immediately obvious. It relies on the fact that an assignment is in itself an expression which returns a value — namely the assigned value.
In other words, you can do y = (x = 10), just as you can do y = (x + 1).

But why it is actually more optimal to do it this way is still not obvious. The trick is that when LSL executes a statement, it keeps the relevant values on the stack. Ending the statement with a ; pops the values off the stack, and they'll have to be fetched back from heap memory to use again. Writing the assignment and comparison in one go avoids this pop – push, and so results in smaller and faster bytecode.

The downside is obviously that the code becomes a weird mishmash of tasks mixed between each other, hard to read, and some consider mixing assignments and evaluations so dangerously error-prone as to be an error in itself.

Danger Zone - List Optimization Trick

One quirk bears mention because it has a huge impact and has been taught as a voodoo trick to simply use because it is just better, but is no longer unconditionally true.

When appending to lists, in LSO (the "old style" LSL bytecode), use

myList = (myList=[]) + myList + [newItem];

This notation cuts down significantly on the memory used by the script, avoiding a duplication in the pass-by-value based compiler, and so halves the peak usage while manipulating lists. The end result is very noticeable; you have a lot more memory practically available to your script if using big lists. This tricky notation can be used for concatenating large strings as well.

However, do not use it when compiling to Mono (which is the default now), where it will result in considerable double work, with no memory saving!
There, simply use the obvious notation

myList += [newItem];

Remember to use the brackets, to indicate that the type is a list element.

Borderline OCD

There are a couple other quirks which are mentioned so often that I've included them here for completeness, even though their practical impact is very, very minimal. They hold true for LSO; I do not know their status in Mono for sure, but they are at least not harmful.

Use "while" instead of "for"

The "for" loop is sub-optimal. Using a similar "while" is preferable.
The usual notation:

for (i = 0; i < 10; i++)

can be written as:

i = 0;
while (i < 10)

which will compile to more efficient code, in both size and speed. Incidentally, notice the difference between the i++ and ++i, which is based on another quirk:

Incremental shorthand quirk

x = x + 1;
x += 1; and 

all compile to the exact same bytecode.


is slower. (Though by a tiny, tiny margin).

—Of course, there is a semantic difference between pre- and postfix, so they are not always interchangeable.