How To Make It Faster?

MAXScript Frequently Asked Questions

The following chapter discusses some general rules and approaches to get the maximum speed out of your code. A MAXScript could do the job as expected but it could do it very slowly or very quickly depending on the writing style of the developer.

In addition, optimizing a script for speed often leads to a cleaner code that is easier to read by others and to maintain by the developer.

Disable Viewport Redraws when making changes to scene objects.

When an object is being changed via MAXScript, 3ds Max will try to update the changes in the viewports as soon as possible. When the changes are many and done quickly in a loop, redraws should be disabled until all changes have been done. You can use the with redraw off() context or a pair of disableSceneRedraw() and enableSceneRedraw() calls to speed up you code.


Refreshing the Viewports

Disable Undo system when possible

new.gif The undo system can consume large amounts of memory and slow down processing.


Test case:

em = mesh()

meshop_setvert = meshop.setvert

fn test7 holdAll =


 local nVerts = getnumverts em

 for i = 1 to nVerts do

  with undo (holdAll or (i == 1 or i == nVerts))

   meshop_setvert em i ([1,1,1]*i)



For 100000 iterations:

test7 true -- 85313 msec., 229 MB

test7 false -- 7609 msec., 11 MB



Every operation that creates an undo record will cost time and memory as it will create internal copies of the changing objects to allow an undo later. When making multiple changes in a loop like for example attaching multiple objects together, the Undo system would attempt to create a single undo copy of each resulting object and might even run out of memory. Disabling Undo explicitly using the Undo off () context can help to speed up scripts significantly in such cases.


The following extreme example shows the difference. In both cases, 1000 boxes will be created using MAXScript and then attached to a single mesh using the attach function. In the first case, each attach call will generate by default an undo record. In the second case, the undo will be explicitly disabled.

Example 1 – Unoptimized Script:

delete $Box* --delete any existing boxes

box_array = #() --initialize an array

for i = 1 to 1000 do --repeat 1000 times

box_array[i] = box pos:[i*30,0,0] --create 1000 boxes


st = timestamp() --get the start time in milliseconds

master_box = convertToMesh box_array[1] --collapse the first box to mesh

for i = 2 to 1000 do --go through all other boxes

attach master_box box_array[i] --attach each box to the mesh

et = timestamp() --stop the time

print (et-st) --print the resulting time

gc() --call Garbage Collection – you will needed it!

On a 800MHz PC, the execution of the attaching part of the script took more than a minute mainly because the system run out of memory and the OS had to swap to disk. Memory usage for 3ds Max went up with about 300 MB!

Example 2 – Optimized Script:

delete $Box*

box_array = #()

for i = 1 to 1000 do

box_array[i] = box pos:[i*30,0,0]

st = timestamp()

undo off --the only difference - the undo

( --has been turned off

master_box = convertToMesh box_array[1]

for i = 2 to 1000 do

attach master_box box_array[i]

)--end undo off

et = timestamp()

print (et-st)

On the same 800MHz machine, the execution of the attaching part of the script took only 3685 milliseconds, almost 20 times faster! There were no memory consumption changes visible in the Windows Task Manager.

Modify Panel can be slow – change to Create Panel when possible.

Some operations on scene objects will reevaluate the modifier stack and also force an update of the Command Panel UI. Even if the viewport redraws are suppressed, the Modify Panel might be forced to redraw. When the modifier stack is not needed by the script (for example for setting Sub-Object levels etc.), it is a good idea to switch to the Create Panel during execution of the script using the max create mode command.

MAX Commands

In 3ds Max 7 and higher, you can also completely disable Modify Panel updates using the new suspendEditing() and resumeEditing() methods:

Command Panels

Use the 'flagForeground' node viewport state method.

The 3ds Max graphics pipeline can use a dual plane technique for speeding up viewport redraws. When only few objects are changing, the graphics subsystem can take a snapshot of the viewport DIB bitmap showing only the non-changing objects which are flagged as background. After that, the graphics pipeline evaluates only the changing objects and draws on top of the background bitmap. These objects are internally flagged as foreground.

The flagForeground method controls the disposition of scene nodes in the viewport foreground/background planes, so you can influence interactive performance on a node. Nodes placed in the foreground plane are redrawn individually and so interactive changes to them through spinners in scripted rollout panels are much faster.


Never get a single pixel when you can get a whole line.

The getPixels and setPixels functions for reading and writing pixels from / to bitmaps are rather slow. When changing a large number or even all pixels of a bitmap, it is a good idea to perform the getPixels and setPixels just once for each horizontal line and do the rest of the work with the resulting array elements.

In the following two examples, a 1000x1000 pixels bitmap should be altered by changing every single pixel. This means reading and writing back one million pixels. In the first example, every single pixel will be read separately, multiplied by a number and written back. We will stop the time in order to compare to the second, optimized version.

Example 1 – Unoptimized Script:

st = timestamp() --get start time in milliseconds

b = bitmap 1000 1000 color:red --create new bitmap with red background

for y = 1 to 1000 do --go through all lines in the bitmap


for x = 1 to 1000 do --go through all pixels in a line


pixels = getPixels b [x-1,y-1] 1 --read one pixel from X,Y

pixels[1] *= (x+y)/2000.0 --alter the pixel

setPixels b [x,y] pixels --write modified pixel back

)--end x loop

)--end y loop

et = timestamp() --get end time in milliseconds

print (et-st) --print time to finish

display b --show a nice black-red gradient


Running this script on a 800MHz system resulted in a time stamp of 21091 milliseconds.

In the second example, instead of reading every single pixel, will be read every line and process all its pixels inside the array returned by getPixels before writing the whole line back to the bitmap. This means we will make only 1000 read and 1000 write calls instead of one million.

Example 2 – Optimized Script:

st = timestamp() --get start time in milliseconds

b = bitmap 1000 1000 color:red --create new bitmap with red background

for y = 1 to 1000 do --go through all lines in the bitmap


pixels = getPixels b [0,y-1] 1000 --read all 1000 pixels of a single line

for x = 1 to 1000 do --go through all pixels in the line


pixels[x] *= (x+y)/2000.0 --alter the pixel

setPixels b [0,y-1] pixels --write back the complete modified line


et = timestamp() --get end time in milliseconds

print (et-st) --print time to finish

display b --show the same nice black-red gradient

Running the optimized script on the same 800MHz system resulted in a time stamp of 10786 milliseconds - more than twice as fast!

Use Mapped Functions instead of For loops when possible.

Mapped functions perform a single operation on multiple objects in a selection using an internal loop and can be faster than a MAXScript loop applying the operation on every object separately. All mappable functions are noted as such in this help file.



Only calculate things once

new.gif MAXScript does no optimization of code. You must do your own optimizations.

Try to avoid running the same calculation more than once, or interrogating the same value in the same node more than once.


Test case:

fn test4a inVal =


local res = 0

for i = 1 to 100 do res += ((inVal * 10) + i)




fn test4b inVal =


local res = 0

local temp = inVal * 10

for i = 1 to 100 do res += (temp + i)




For 100000 iterations:

test4a 0   -- 20562 msec.

test4b 0   -- 17797 msec.


In another example, a typical example of a script that you want to be as fast as possible is a Particle Flow Script Operator. In a typical Script Operator, you usually go through all particles in the Particle Container of the current Event and perform some operations on each one of them.

The Proceed handler typically looks like

Good Code

on Proceed pCont do


count = pCont.NumParticles()

for i in 1 to count do


pCont.particleIndex = i

pCont.particleVector = pCont.ParticlePosition



Note that the variable 'count', containing the number of particles to be processed, is evaluated only once and then used as the top limit of the i loop.

Writing the same as

Bad Code

on Proceed pCont do


for i in 1 to pCont.NumParticles() do


pCont.particleIndex = i

pCont.particleVector = pCont.ParticlePosition



would be a bad idea, because in the case of 1 million particles, the expression pCont.NumParticles() will be evaluated 1 million times instead of just once!

Cache freqeuntly used functions and objects

new.gif You can store frequently used functions and objects in user variables to faster access.


Test case:

ep = converttopoly (mesh())    --node

ep_bo = ep.baseobject      --ediable poly

polyop_getvert = polyop.getvert    --structure method

IEditablePoly = ep_bo.EditablePoly   --FPS interface

IEditablePoly_GetNumMapChannels =

IEditablePoly.GetNumMapChannels   --FPS interface


For 100000 iterations:

polyop.getvert ep 1    -- 470 msec.

polyop_getvert ep 1    -- 79 msec.

polyop.getvert ep_bo 1   -- 48 msec.

polyop_getvert ep_bo 1   -- 0 msec.

ep.EditablePoly.GetNumMapChannels() -- 76532 msec.

ep.GetNumMapChannels()   -- 72141 msec.

ep_bo.EditablePoly.GetNumMapChannels() -- 36688 msec.

ep_bo.GetNumMapChannels()   -- 34219 msec.

IEditablePoly.GetNumMapChannels() -- 6454 msec.

IEditablePoly_GetNumMapChannels() -- 0 msec.



Test case:

gs = geosphere()

gs_bo = gs.baseobject

theBend = bend()

addmodifier gs theBand


For 100000 iterations:

gs.radius     -- 718 msec.

gs.baseobject.radius    -- 828 msec.

gs_bo.radius      -- 688 msec.

gs.modifiers[1].angle    -- 718 msec.

gs.modifiers[#bend].angle   -- 1187 msec.

theBend.angle     -- 609 msec.


See also

Increasing Performance when Searching for Interfaces and Methods


Dice your data into smaller pieces.

If you are sorting, searching or correlating data within a large array, you'll get there much faster by cutting it up into smaller pieces, then maybe cutting again. If you're trying to find the closest neighbors of each point3, for example, you would want to dice the original array into a 3D grid of spatial subdivisions.

Use bitArrays instead of Arrays when possible.

Many mesh-related methods operate on bitArrays. A bitArray stores only true and false flags and is very memory-efficient, but on the other hand, appending new elements to a bitArray is much slower than appending new elements to a regular array.

If the bitArray is of fixed size (for example returned by another method), using a bitArray is better than using a regular array.

If the final size of the array or bitArray is known, you can predeclare the array to reserve memory for as many elements by assigning a value to the last element as described below. Setting the values of arrays of bitArrays using indexed access to their elements after that is very fast. In this case, if you can need only true and false values, use a bitArray.

BitArray Values

Array Values

Pre-initialize arrays when final size is known

When adding elements to an array using the append method, a copy of the original array is being created in memory before the new array is created. When the size of an array is known before the array is actually used, it is a good practice to pre-initialize the array in memory by assigning the last element of the array to some temporary value. This will create an array of the desired size in memory and will let you simply assign values to any element of the array using indexed access without the memory overhead of the append method.

For example, instead of using something like

MyArray = #() --creates an empty array

for i = 1 to 100 do

append MyArray (random 1 100) --append each element

you can use

MyArray = #()

MyArray[100] = 0 --initialize a 100 elements array in memory

for i = 1 to 100 do

MyArray[i] = random 1 100 --assign to predefined array

Recursive functions can be faster

A recursive function is a function that calls itself in order to perform repetitive tasks.

The following scripted function returns a list of the animated subAnims of the object passed as parameter. The script works well and is not too slow.

Non-Recursive Version

fn getAllAnimatedProperties theObject =


scan_properties = #(theObject)

animated_props = #()

cnt = 0

while cnt < scan_properties.count do


cnt +=1

currentObj = scan_properties[cnt]

if try(currentObj.isAnimated)catch(false) do

append animated_props currentObj

for i = 1 to currentObj.numSubs do

append scan_properties currentObj[i]



getAllAnimatedProperties $

Now take a look at this code:

Recursive Version

animated_props = #()

fn getAnimatedProps theObject =


if try(theObject.isAnimated)catch(false) do

append animated_props theObject

for i = 1 to theObject.numSubs do

getAnimatedProps theObject[i]


getAnimatedProps $

The recursive code does the same job, but is much shorter and almost 25% faster! (In order to get some usable measurement, both scripts were executed 100.000 times - the first took 13.875 seconds, the recursive version only 10.656 seconds.

matchPattern is faster than findString

See also

Frequently Asked Questions


Do not use return, break, exit or continue

new.gif Return, break, exit, continue and throw are implemented using C++ exception.

C++ exception are SLOW!


Test cases:

fn test1a v = (if v == true do return 1; 0)

fn test1b v = (if v == true then 1 else 0)


For 100000 iterations:

test1a true  -- 15890 msec.

test1a false  -- 78 msec.

test1b true  -- 47 msec.

test1b false  -- 62 msec.


Test cases:

fn test2a =


local res

for i = 1 to 1000 do

  if i == 10 do (res = i; break;)



fn test2b =


local notfound = true, res

for i = 1 to 1000 while notfound do

  if i == 10 do (res = i; notfound = false;)




For 100000 iterations:

test2a()   -- 84265 msec.

test2b()   -- 1359 msec.



Use StringStream to build large string

new.gif If building strings, use a StringStream value to accumulate the string and then convert to a string.


Each string addition creates a new string.

For Example:

a = "AAA"

b = a + a + a +a + a + a

--Creates 6 strings of length 3, 6, 9, 12, 15, 18


a = "AAA"

b = (a + a + a) + (a + a + a)

--Creates 6 strings of length 3, 6, 9, 6, 9, 18


Test Cases:

fn test5a =


 local a = ""

 for i = 1 to 100 do a += (i as string)



fn test5b =


 local ss = stringstream ""

for i = 1 to 100 do format "%" (i as string) to:ss

ss as string



fn test5c =


 local ss = stringstream "", fmt = "%"

for i = 1 to 100 do format fmt (i as string) to:ss

ss as string



fn test5d =


 local ss = stringstream "", fmt = "%"

for i = 1 to 100 do format fmt i to:ss

ss as string



For 100000 iterations:

test5a()     -- 58875 msec., 505 MB

test5b()     -- 54672 msec., 39.2 MB

test5c()     -- 41125 msec., 29.2 MB

test5d()     -- 13532 msec., 10.0 MB



Use name values instead of strings when possible

new.gif Each use of a string literal requires creating a new string value.


Problem case:

fn test = append "A" "B"

test() à "AB"

test() à "ABB"


Test case:

fn test6 v = ()


For 100000 iterations:

test6 "A"  -- 125 msec.

test6 #a  -- 16 msec.



Try not to use Execute function if there is an alternative

new.gif Very few cases require the use of the execute function.

MAXScript has many introspection methods and properties:


Valid cases are: