Upgrade to Pro — share decks privately, control downloads, hide ads and more …

The 3D Engine Series Part 3: Performance optimization (CopenhagenJS)

Richard Olsson
September 20, 2012

The 3D Engine Series Part 3: Performance optimization (CopenhagenJS)

This presentation is the third in a series of presentations revolving around the WebGL 3D engine that I'm currently experimenting with. Unlike the previous two this focus less on the engine itself and more on some general performance optimization tricks that I employ within the engine, but that could be used in any project.

Richard Olsson

September 20, 2012
Tweet

Other Decks in Technology

Transcript

  1. I’m Richard, I develop 3D engines (among other things) This

    is part 3 in a series of presentations Follows the progress of building a 3D engine (WebGL) 3D engine series, part 3 - @richardolsson
  2. Away3D.js is a personal experiment of mine Porting existing Flash

    3D rendering engine “Away3D” to Javascript May or may not become an official Away3D release 3D engine series, part 3 - @richardolsson
  3. Continued implementing lighting Improved shader architecture Got basic diffuse and

    specular shading (and point lights) working 3D engine series, part 3 - @richardolsson
  4. Achieving the same results more efficiently 3D engine series, part

    3 - @richardolsson This chunk of code comes from Quake 3, and is an example of optimization by approximation (and magic.) The inverse square root is calculated quickly by treating a float (in memory) as an integer, shifting it one bit and subtracting it from a “magic” constant.
  5. Achieving the same results more efficiently Achieving approximately the same

    results more efficiently 3D engine series, part 3 - @richardolsson This chunk of code comes from Quake 3, and is an example of optimization by approximation (and magic.) The inverse square root is calculated quickly by treating a float (in memory) as an integer, shifting it one bit and subtracting it from a “magic” constant.
  6. Achieving the same results more efficiently float Q_rsqrt( float number

    ) { long i; float x2, y; const float threehalfs = 1.5F; x2 = number * 0.5F; y = number; i = * ( long * ) &y; // evil bit level hacking i = 0x5f3759df - ( i >> 1 ); // what the fuck? y = * ( float * ) &i; y = y * ( threehalfs - ( x2 * y * y ) ); // 1st iteration return y; } Achieving approximately the same results more efficiently 3D engine series, part 3 - @richardolsson This chunk of code comes from Quake 3, and is an example of optimization by approximation (and magic.) The inverse square root is calculated quickly by treating a float (in memory) as an integer, shifting it one bit and subtracting it from a “magic” constant.
  7. Sometimes yes, like when you need to do heavy things

    3D engine series, part 3 - @richardolsson
  8. Sometimes yes, like when you need to do heavy things

    Sometimes maybe, like when you don’t know how your code will be used 3D engine series, part 3 - @richardolsson
  9. Sometimes yes, like when you need to do heavy things

    Sometimes maybe, like when you don’t know how your code will be used Sometimes less so, like when you have a well-defined problem with a well-tested solution 3D engine series, part 3 - @richardolsson
  10. Simple example: Print a user’s friends as “his/her friend John

    Doe” 3D engine series, part 3 - @richardolsson The evaluation of the if statement does not change within the loop (because user.gender is never modified inside the loop.) That means that the if statement could be moved outside of the loop, thereby preventing branching inside the loop.
  11. Simple example: Print a user’s friends as “his/her friend John

    Doe” function printUserFriends(user, friends) { var i; for (i=0; i<friends.length; i++) { if (user.gender == ‘male’) { console.log(‘his friend ‘+friends[i]); } else { console.log(‘her friend ‘+friends[i]); } } } 3D engine series, part 3 - @richardolsson The evaluation of the if statement does not change within the loop (because user.gender is never modified inside the loop.) That means that the if statement could be moved outside of the loop, thereby preventing branching inside the loop.
  12. Simple example: Print a user’s friends as “his/her friend John

    Doe” function printUserFriends(user, friends) { var i; for (i=0; i<friends.length; i++) { if (user.gender == ‘male’) { console.log(‘his friend ‘+friends[i]); } else { console.log(‘her friend ‘+friends[i]); } } } function printUserFriends(user, friends) { var i, prefix; if (user.gender == ‘male’) prefix = ‘his friend ‘; else prefix = ‘her friend ‘; for (i=0; i<friends.length; i++) { console.log(prefix+friends[i]); } } 3D engine series, part 3 - @richardolsson The evaluation of the if statement does not change within the loop (because user.gender is never modified inside the loop.) That means that the if statement could be moved outside of the loop, thereby preventing branching inside the loop.
  13. Another example: Manage a 100-item queue of numbers, and keep

    track of average value 3D engine series, part 3 - @richardolsson A naive algorithm for this is to recalculate the sum everytime something is added (or removed.) A better way is to keep track of the sum as objects are added/removed by simply adding to and subtracting from it.
  14. Another example: Manage a 100-item queue of numbers, and keep

    track of average value 3D engine series, part 3 - @richardolsson var avg, sum, queue = []; function add(val) { queue.push(val); sum = 0; for (var i=0; i<queue.length; i++) { sum += queue[len]; } avg = sum/queue.length; }; A naive algorithm for this is to recalculate the sum everytime something is added (or removed.) A better way is to keep track of the sum as objects are added/removed by simply adding to and subtracting from it.
  15. Another example: Manage a 100-item queue of numbers, and keep

    track of average value var avg, sum = 0, queue = []; function add(val) { queue.push(val); sum += val; if (queue.length > 100) { sum -= queue.shift(); } avg = sum/queue.length; }; 3D engine series, part 3 - @richardolsson var avg, sum, queue = []; function add(val) { queue.push(val); sum = 0; for (var i=0; i<queue.length; i++) { sum += queue[len]; } avg = sum/queue.length; }; A naive algorithm for this is to recalculate the sum everytime something is added (or removed.) A better way is to keep track of the sum as objects are added/removed by simply adding to and subtracting from it.
  16. Some basics Instantiating objects (allocating memory) and deleting them (freeing

    memory) takes time Instantiation takes even more time because of internal initialization Javascript relies on garbage collection for memory management Garbage collectors figure out when objects are no longer used and delete them 3D engine series, part 3 - @richardolsson
  17. The problem Garbage collected languages let you not care about

    freeing memory, but also prevents you from controlling when that happens Garbage collection can be expensive, and happen at unsuitable times This makes memory management more than just a memory consumption issue 3D engine series, part 3 - @richardolsson
  18. 3D engine series, part 3 - @richardolsson Which graph portrays

    the least optimal memory usage? The leftmost one is optimal because no (or very little) allocation and deallocation happens. The middle one is bad if it goes on forever (because memory will be exhausted), but ok if it stops at some point. The rightmost graph indicates that a lot of objects are be created only to then be freed soon after, which hints at unnecessary use of instantiation and garbage collector resources.
  19. 3D engine series, part 3 - @richardolsson Which graph portrays

    the least optimal memory usage? The leftmost one is optimal because no (or very little) allocation and deallocation happens. The middle one is bad if it goes on forever (because memory will be exhausted), but ok if it stops at some point. The rightmost graph indicates that a lot of objects are be created only to then be freed soon after, which hints at unnecessary use of instantiation and garbage collector resources.
  20. 3D engine series, part 3 - @richardolsson Which graph portrays

    the least optimal memory usage? The leftmost one is optimal because no (or very little) allocation and deallocation happens. The middle one is bad if it goes on forever (because memory will be exhausted), but ok if it stops at some point. The rightmost graph indicates that a lot of objects are be created only to then be freed soon after, which hints at unnecessary use of instantiation and garbage collector resources.
  21. Solutions Avoid instantiating objects! Reuse objects whenever possible, to prevent

    the need for collection 3D engine series, part 3 - @richardolsson
  22. Example: Create methods for spawning and killing enemies in a

    game 3D engine series, part 3 - @richardolsson Instead of creating an enemy whenever one should spawn, and removing those that die (so the GC collects them), one can pool enemy objects. First create 100 (or any relevant number) dead enemies, and just mark them as alive when they spawn. When one dies, mark it as dead, in this case by moving it to the other list.
  23. Example: Create methods for spawning and killing enemies in a

    game var enemies = []; function spawn() { // Create new enemy var enemy = new Enemy(); enemies.push(enemy); } function kill(enemy) { // Remove from array, will be // garbage collected eventually var idx = enemies.indexOf(enemy); enemies.splice(idx, 1); } 3D engine series, part 3 - @richardolsson Instead of creating an enemy whenever one should spawn, and removing those that die (so the GC collects them), one can pool enemy objects. First create 100 (or any relevant number) dead enemies, and just mark them as alive when they spawn. When one dies, mark it as dead, in this case by moving it to the other list.
  24. Example: Create methods for spawning and killing enemies in a

    game var enemies = []; function spawn() { // Create new enemy var enemy = new Enemy(); enemies.push(enemy); } function kill(enemy) { // Remove from array, will be // garbage collected eventually var idx = enemies.indexOf(enemy); enemies.splice(idx, 1); } var i, poolSize = 100, deadEnemies = [], livingEnemies = []; // Create object pool for (i=0; i<poolSize; i++) { deadEnemies[i] = new Enemy(); } function spawn() { var enemy = deadEnemies.pop(); enemy.reset(); livingEnemies.push(enemy); } function kill(enemy) { deadEnemies.push(enemy); // Remove enemy, won’t be collected because // reference is still in deadEnemies var idx = livingEnemies.indexOf(enemy); livingEnemies.splice(idx, 1); } 3D engine series, part 3 - @richardolsson Instead of creating an enemy whenever one should spawn, and removing those that die (so the GC collects them), one can pool enemy objects. First create 100 (or any relevant number) dead enemies, and just mark them as alive when they spawn. When one dies, mark it as dead, in this case by moving it to the other list.
  25. Example: Matrix multiplication (taken from away3d.js) 3D engine series, part

    3 - @richardolsson Some functions create objects internally, which makes it impossible for the user of the function to prevent excessive instantiation (and subsequently garbage collection.) Instead of creating and returning, accept an argument that will be modified and then returned, leaving memory management in the hands of the user of your API (where educated decisions can be made.)
  26. Example: Matrix multiplication (taken from away3d.js) Matrix3D.Translation = function(x, y,

    z) { mtx = new Matrix3D(); mtx.data[0] = 1; mtx.data[5] = 1; mtx.data[10] = 1; mtx.data[12] = x; mtx.data[13] = y; mtx.data[14] = z; return mtx; }; // Usage: m = Matrix3D.Translation(1, 10, 25); 3D engine series, part 3 - @richardolsson Some functions create objects internally, which makes it impossible for the user of the function to prevent excessive instantiation (and subsequently garbage collection.) Instead of creating and returning, accept an argument that will be modified and then returned, leaving memory management in the hands of the user of your API (where educated decisions can be made.)
  27. Example: Matrix multiplication (taken from away3d.js) Matrix3D.Translation = function(x, y,

    z) { mtx = new Matrix3D(); mtx.data[0] = 1; mtx.data[5] = 1; mtx.data[10] = 1; mtx.data[12] = x; mtx.data[13] = y; mtx.data[14] = z; return mtx; }; // Usage: m = Matrix3D.Translation(1, 10, 25); Matrix3D.Translation = function(x, y, z, mtx) { mtx = mtx || new Matrix3D(); mtx.data[0] = 1; mtx.data[5] = 1; mtx.data[10] = 1; mtx.data[12] = x; mtx.data[13] = y; mtx.data[14] = z; return mtx; }; // Usage: m = new away3d.Matrix3D(); // Or use existing Matrix3D.Translation(1, 10, 25, m); 3D engine series, part 3 - @richardolsson Some functions create objects internally, which makes it impossible for the user of the function to prevent excessive instantiation (and subsequently garbage collection.) Instead of creating and returning, accept an argument that will be modified and then returned, leaving memory management in the hands of the user of your API (where educated decisions can be made.)
  28. 3D engine series, part 3 - @richardolsson Taking two snapshots

    and comparing the amount of objects that differ between the two can indicate whether objects are being reused well. These two snapshots are a couple of seconds apart while rendering a real-time 3D scene. Notice how no new matrices (or any other object) are being created.
  29. 3D engine series, part 3 - @richardolsson Taking two snapshots

    and comparing the amount of objects that differ between the two can indicate whether objects are being reused well. These two snapshots are a couple of seconds apart while rendering a real-time 3D scene. Notice how no new matrices (or any other object) are being created.
  30. Be clear to the compiler about data types As an

    example, what happens if we use floating point numbers in a for-loop? 3D engine series, part 3 - @richardolsson Two almost identical loops, although one uses a floating point iterator (0.5).
  31. Be clear to the compiler about data types As an

    example, what happens if we use floating point numbers in a for-loop? var i0, sum0=0; for (i0=0; i0<1000; i0++) { sum0 += 2; } var i1, sum1=0; for (i1=0.5; i1<1000; i1++) { sum1 += 2; } 3D engine series, part 3 - @richardolsson Two almost identical loops, although one uses a floating point iterator (0.5).
  32. 3D engine series, part 3 - @richardolsson When mixing integers

    and floating point values, all are converted to floating point. This can be proven (in Chrome) by looking at the middle-level IR of Javascript before it gets compiled to native machine code. The “d” tag on all relevant registers indicates that the numbers have been tagged as floating point, even those that are really integers (e.g. constants 1 and 1000.)
  33. 3D engine series, part 3 - @richardolsson When mixing integers

    and floating point values, all are converted to floating point. This can be proven (in Chrome) by looking at the middle-level IR of Javascript before it gets compiled to native machine code. The “d” tag on all relevant registers indicates that the numbers have been tagged as floating point, even those that are really integers (e.g. constants 1 and 1000.)
  34. Use consistent interfaces This allows the runtime to create “hidden

    classes” 3D engine series, part 3 - @richardolsson In one case, objects created by the Point constructor might sometimes have two, sometimes three properties. In the other case, all Point objects will always have three properties, even if the third is undefined. This allows the compiler to optimize addPoints2D() since it always uses the same internal type (called a “hidden class”.)
  35. Use consistent interfaces This allows the runtime to create “hidden

    classes” function Point(x, y, z) { this.x = x; this.y = y; if (z != undefined) this.z = z; } function addPoints2D(p0, p1) { p0.x += p1.x; p0.y += p1.y; } 3D engine series, part 3 - @richardolsson In one case, objects created by the Point constructor might sometimes have two, sometimes three properties. In the other case, all Point objects will always have three properties, even if the third is undefined. This allows the compiler to optimize addPoints2D() since it always uses the same internal type (called a “hidden class”.)
  36. Use consistent interfaces This allows the runtime to create “hidden

    classes” function Point(x, y, z) { this.x = x; this.y = y; if (z != undefined) this.z = z; } function addPoints2D(p0, p1) { p0.x += p1.x; p0.y += p1.y; } function Point(x, y, z) { this.x = x; this.y = y; this.z = z; } function addPoints2D(p0, p1) { p0.x += p1.x; p0.y += p1.y; } 3D engine series, part 3 - @richardolsson In one case, objects created by the Point constructor might sometimes have two, sometimes three properties. In the other case, all Point objects will always have three properties, even if the third is undefined. This allows the compiler to optimize addPoints2D() since it always uses the same internal type (called a “hidden class”.)
  37. Don’t use try/catch RTE handling disables a lot of runtime

    optimizations Instead, prevent errors by fail-proofing your code (e.g. look for null or undefined before accessing a property) 3D engine series, part 3 - @richardolsson
  38. 3D engine series, part 3 - @richardolsson The try/catch results

    are two small to even be visible, that’s how slow they are.
  39. Some basics In unmanaged (low-level) languages, calling a function is

    a simple thing: Store current position in program, then jump to function start. In managed (higher-level) languages, function calls often include many checks, state changes and jumps. Functions sometimes prevent compiler from optimizing by restructuring code 3D engine series, part 3 - @richardolsson
  40. function printUserFriends(user, friends) { var i; for (i=0; i<friends.length; i++)

    { if (user.gender == ‘male’) { console.log(‘his friend ‘+friends[i]); } else { console.log(‘her friend ‘+friends[i]); } } } 3D engine series, part 3 - @richardolsson Revisiting the first example, it’s not unthinkable that Javascript compilers could soon perform structural optimizations themselves. However, moving the loop body into a separate function could prevent this from happening because the compiler doesn’t see the full context.
  41. function printUserFriends(user, friends) { var i; if (user.gender == ‘male’)

    { for (i=0; i<friends.length; i++) { console.log(‘his friend ‘+friends[i]); } } else { for (i=0; i<friends.length; i++) { console.log(‘her friend ‘+friends[i]); } } } function printUserFriends(user, friends) { var i; for (i=0; i<friends.length; i++) { if (user.gender == ‘male’) { console.log(‘his friend ‘+friends[i]); } else { console.log(‘her friend ‘+friends[i]); } } } 3D engine series, part 3 - @richardolsson Revisiting the first example, it’s not unthinkable that Javascript compilers could soon perform structural optimizations themselves. However, moving the loop body into a separate function could prevent this from happening because the compiler doesn’t see the full context.
  42. function printUserFriends(user, friends) { var i; if (user.gender == ‘male’)

    { for (i=0; i<friends.length; i++) { console.log(‘his friend ‘+friends[i]); } } else { for (i=0; i<friends.length; i++) { console.log(‘her friend ‘+friends[i]); } } } function printUserFriends(user, friends) { var i; for (i=0; i<friends.length; i++) { if (user.gender == ‘male’) { console.log(‘his friend ‘+friends[i]); } else { console.log(‘her friend ‘+friends[i]); } } } 3D engine series, part 3 - @richardolsson Revisiting the first example, it’s not unthinkable that Javascript compilers could soon perform structural optimizations themselves. However, moving the loop body into a separate function could prevent this from happening because the compiler doesn’t see the full context.
  43. function printUserFriends(user, friends) { var i; if (user.gender == ‘male’)

    { for (i=0; i<friends.length; i++) { console.log(‘his friend ‘+friends[i]); } } else { for (i=0; i<friends.length; i++) { console.log(‘her friend ‘+friends[i]); } } } function printUserFriends(user, friends) { var i; for (i=0; i<friends.length; i++) { if (user.gender == ‘male’) { console.log(‘his friend ‘+friends[i]); } else { console.log(‘her friend ‘+friends[i]); } } } 3D engine series, part 3 - @richardolsson function printUserFriends(user, friends) { var i; for (i=0; i<friends.length; i++) { printUserFriend(user, friends[i]) } } function printUserFriend(user, friend) { if (user.gender == ‘male’) { console.log(‘his friend ‘+friend); } else { console.log(‘her friend ‘+friend); } } Revisiting the first example, it’s not unthinkable that Javascript compilers could soon perform structural optimizations themselves. However, moving the loop body into a separate function could prevent this from happening because the compiler doesn’t see the full context.
  44. function printUserFriends(user, friends) { var i; if (user.gender == ‘male’)

    { for (i=0; i<friends.length; i++) { console.log(‘his friend ‘+friends[i]); } } else { for (i=0; i<friends.length; i++) { console.log(‘her friend ‘+friends[i]); } } } function printUserFriends(user, friends) { var i; for (i=0; i<friends.length; i++) { if (user.gender == ‘male’) { console.log(‘his friend ‘+friends[i]); } else { console.log(‘her friend ‘+friends[i]); } } } 3D engine series, part 3 - @richardolsson function printUserFriends(user, friends) { var i; for (i=0; i<friends.length; i++) { printUserFriend(user, friends[i]) } } function printUserFriend(user, friend) { if (user.gender == ‘male’) { console.log(‘his friend ‘+friend); } else { console.log(‘her friend ‘+friend); } } Revisiting the first example, it’s not unthinkable that Javascript compilers could soon perform structural optimizations themselves. However, moving the loop body into a separate function could prevent this from happening because the compiler doesn’t see the full context.
  45. The problem The overhead of functions in JS makes them

    expensive However, functions are necessary to structure a program well 3D engine series, part 3 - @richardolsson
  46. Solutions Avoid function calls where you don't need them, inline

    manually when possible Keep functions short, to allow automatic inlining Don't unnecessarily do things that can prevent automatic inlining, like recursion or argument modification 3D engine series, part 3 - @richardolsson
  47. Simple example of manual inlining 3D engine series, part 3

    - @richardolsson In this naive example of inlining, the sum() function only ever adds 1+1, so can very simply be inlined, i.e. the body of the function can be used directly instead of invoking the function. This type of inlining is usually performed automatically by the compiler.
  48. Simple example of manual inlining var sum = function() {

    return 1+1; } result = sum(); 3D engine series, part 3 - @richardolsson In this naive example of inlining, the sum() function only ever adds 1+1, so can very simply be inlined, i.e. the body of the function can be used directly instead of invoking the function. This type of inlining is usually performed automatically by the compiler.
  49. Simple example of manual inlining var sum = function() {

    return 1+1; } result = sum(); result = 1+1; 3D engine series, part 3 - @richardolsson In this naive example of inlining, the sum() function only ever adds 1+1, so can very simply be inlined, i.e. the body of the function can be used directly instead of invoking the function. This type of inlining is usually performed automatically by the compiler.
  50. 3D engine series, part 3 - @richardolsson These are the

    results when automatic inlining doesn’t happen, which it would if you would run this function often enough in a loop. Benchmark at http://jsperf.com/function-vs-inline-2
  51. Some basics Accessing a field of an object is a

    detour compared to accessing a local variable This is even more true in the case of polymorphic (prototype-based) objects 3D engine series, part 3 - @richardolsson
  52. The problem Oftentimes the same property is accessed over and

    over, even though it's value is unlikely to change 3D engine series, part 3 - @richardolsson
  53. Example: getter vs field vs local variable 3D engine series,

    part 3 - @richardolsson The dummy list implementation here has two ways of checking it’s length, the lengthField and the length getter. The three approaches to iterating it on the right are used to measure relative performance between using length, lengthField and caching the value locally.
  54. Example: getter vs field vs local variable var list =

    (function() { var i, numItems = 1000, o = { _all: [], lengthField: numItems, itemAt: function(idx) { return this._all[idx]; } }; Object.defineProperty(o, 'length', { get: function() { return numItems; } }); for (i=0; i<numItems; i++) { o._all[i] = Math.random(); } return o; })(); 3D engine series, part 3 - @richardolsson The dummy list implementation here has two ways of checking it’s length, the lengthField and the length getter. The three approaches to iterating it on the right are used to measure relative performance between using length, lengthField and caching the value locally.
  55. Example: getter vs field vs local variable var list =

    (function() { var i, numItems = 1000, o = { _all: [], lengthField: numItems, itemAt: function(idx) { return this._all[idx]; } }; Object.defineProperty(o, 'length', { get: function() { return numItems; } }); for (i=0; i<numItems; i++) { o._all[i] = Math.random(); } return o; })(); var i, sum = 0; for (i=0; i<list.length; i++) { sum += list.itemAt(i); } var i, sum = 0; for (i=0; i<list.lengthField; i++) { sum += list.itemAt(i); } var i, sum = 0, len = list.length; for (i=0; i<len; i++) { sum += list.itemAt(i); } 3D engine series, part 3 - @richardolsson The dummy list implementation here has two ways of checking it’s length, the lengthField and the length getter. The three approaches to iterating it on the right are used to measure relative performance between using length, lengthField and caching the value locally.
  56. Avoid getters and setters (They rely on both object look-ups

    and functions, both of which are slow) 3D engine series, part 3 - @richardolsson
  57. Avoid polymorphism using prototypes (It requires prototype look-ups which are

    essentially several ordinary object look-ups in a row) 3D engine series, part 3 - @richardolsson
  58. Closures are faster than object access (If you need to

    store state in an object, using closures will be faster than accessing the state through this) 3D engine series, part 3 - @richardolsson Related benchmark: http://jsperf.com/class-patterns-single-instance-method-execution
  59. Avoid shuffling and copying memory (Try to avoid concatenating strings,

    and splicing arrays) 3D engine series, part 3 - @richardolsson
  60. Pro tip: splicing when order doesn’t matter (for example in

    an object pool) 3D engine series, part 3 - @richardolsson The splice method has to move everything in the array to “condense” it when something in the center is removed. The “fast splice” approach instead moves the last object to fill the gap created by the one that was removed. This means order is changed, but when that doesn’t matter this approach is vastly more performant. Great for use in object pools and game object collections.
  61. Pro tip: splicing when order doesn’t matter (for example in

    an object pool) var i; for (i = 0; i < 5; i++) { arr.splice(i, 1); } 3D engine series, part 3 - @richardolsson The splice method has to move everything in the array to “condense” it when something in the center is removed. The “fast splice” approach instead moves the last object to fill the gap created by the one that was removed. This means order is changed, but when that doesn’t matter this approach is vastly more performant. Great for use in object pools and game object collections.
  62. Pro tip: splicing when order doesn’t matter (for example in

    an object pool) var i; for (i = 0; i < 5; i++) { arr.splice(i, 1); } var i; for (i = 0; i < 5; i++) { var last = arr.length - 1; arr[i] = arr[last]; arr[last] = null; } 3D engine series, part 3 - @richardolsson The splice method has to move everything in the array to “condense” it when something in the center is removed. The “fast splice” approach instead moves the last object to fill the gap created by the one that was removed. This means order is changed, but when that doesn’t matter this approach is vastly more performant. Great for use in object pools and game object collections.