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.
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.
The undo system can consume large amounts of memory and slow down processing.
MAXScript commands run from Listener. Script Editor or Macro Script are run within an undo on context.
MAXScript commands run from scripted UI controls are not run within an undo on context.
Do not delete nodes with undo off, unless you also created the nodes with undo off and did not do anything to the node with undo on.
When working with meshes, you typically want to use meshop methods as they support undo/redo. But typically you do not want to store undo records for all operations in a loop, just the first and last.
When working with EPoly, you want to store undo records for all operations in a loop.
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.
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.
In 3ds Max 7 and higher, you can also completely disable Modify Panel updates using the new suspendEditing() and resumeEditing() methods:
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.
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!
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.
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)
res
)
fn test4b inVal =
(
local res = 0
local temp = inVal * 10
for i = 1 to 100 do res += (temp + i)
res
)
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!
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
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.
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.
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
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]
)
animated_props
)
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.
When searching for a substring inside a string, using the matchPattern() method is faster than using findString(). Executing the same method one million times on the same PC resulted in
3.7 seconds execution time for matchPattern
5.9 seconds execution time for findString
5.0 seconds execution time for subString when comparing the first 3 characters.
See also
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;)
res
)
fn test2b =
(
local notfound = true, res
for i = 1 to 1000 while notfound do
if i == 10 do (res = i; notfound = false;)
res
)
For 100000 iterations:
test2a() -- 84265 msec.
test2b() -- 1359 msec.
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