Does Caching work?

The question of caching usefulness was raised. It obviously depends on how much math/etc you are doing, but, even in the case of a simple bit of math, on a small number of LEDs, I see sufficient improvements to make it seem worthwhile, so more math, more LEDs, it would only improve.

var cache = array(pixelCount)
var usecache = 0

for (i = 0; i < pixelCount; i++){
  cache[i] = array(3) // to ensure it's a "array within an array" cache, 
  //  a simple array lookup might be even faster, but this allows for multiple cached values 
}

export function render2D(index,x,y) {
    if (usecache == 0) {
      a = hypot(x-.5,y-.5)
      cache[index][0] = a
      if (index == pixelCount - 1) { usecache = 1} // change from 1 to 0 to disable using cache
    } else {
      a = cache[index][0]    
    }
    hsv(a, 1, 1)
}

Cache enabled v3, 256 leds 16x16 matrix: 100 fps
Cache disabled same setup: 86 fps
Cache disabled, and array line cache[index][0] = a commented out too: 91.5 FPS

So, by removing the array write and not using the cache, we are still slower than the cached version, but we still have logic in there, and variables.

So now, let’s do the minimal version:

export function render2D(index,x,y) {
    hsv(hypot(x-.5,y-.5), 1, 1)
}

and that’s 103.5 FPS. Hmm…

FPS golfing is called for, I think. I suspect caching makes sense in non-trivial cases, where lots of math or other logic happens. But removing variables, and doing it as directly as possible COULD be faster, or at least equivalent. But that’s just one built in function (hypot)… what if we add 5 or 6 math operations?

export function render2D(index,x,y) {
    hsv(hypot(x-.5,y-.5), sin(x+y), cos(x-y)*(x+y))
}

that’s 90.7 FPS

var cache = array(pixelCount)
var usecache = 0

for (i = 0; i < pixelCount; i++){
  cache[i] = array(3) // to ensure it's a "array within an array" cache, 
  //  a simple array lookup might be even faster, but this allows for multiple cached values 
}

export function render2D(index,x,y) {
    if (usecache == 0) {
      a = hypot(x-.5,y-.5)
      b = sin(x+y)
      c = cos(x-y)*(x+y)
      cache[index][0] = a
      cache[index][1] = b
      cache[index][2] = c
      if (index == pixelCount - 1) { usecache = 1} // change from 1 to 0 to disable using cache
    } else {
      a = cache[index][0]    
      b = cache[index][1]    
      c = cache[index][2]    
    }
    hsv(a, b, c)
}

putting that into a caching version, where we cache a,b,c: 87.5 FPS
Closing to parity, but still slightly slower.

Timing the reading of an array of an array, comparing it to various math timing, might show us that there is a lower bound where we should do the math rather than store it and read it back… TBD.

1 Like

BTW, and I say this with a bit of caution, this is one place where I use the render-function-as-a-variable trick. Won’t be necessary once the mapPixels API is in, but this lets you do a pass over the pixels for “free” (no cost in future renders).

~92 fps:

var cache = array(pixelCount)

for (i = 0; i < pixelCount; i++){
  cache[i] = array(3) // to ensure it's a "array within an array" cache, 
  //  a simple array lookup might be even faster, but this allows for multiple cached values 
}

function renderCache(index, x, y) {
  a = cache[index][0]    
  b = cache[index][1]    
  c = cache[index][2]  
  hsv(a, b, c)
}

function renderCalc(index, x, y) {
  a = hypot(x-.5,y-.5)
  b = sin(x+y)
  c = cos(x-y)*(x+y)
  cache[index][0] = a
  cache[index][1] = b
  cache[index][2] = c
  
  //now that this pixel is cached, call out to the function that draws from cache
  //so the first frame isn't blank (or code duplicated)
  renderCache(index, x, y)
  
  if (index == pixelCount - 1) {
    render2D = renderCache //at the end of the last pixel, swap to the cache render
  }
}

export var render2D = renderCalc //start with the calc version of render

Just about everything has some overhead, so if you are shaving cycles, fewer array lookups using single dimensional arrays, and avoiding temporary variables will be faster. It might be a style thing, but I prefer to use parallel arrays for different attributes than to pack an array into an array as if it was an object.

~105 FPS:

var hues = array(pixelCount)
var saturations = array(pixelCount)
var values = array(pixelCount)

function renderCache(index, x, y) {
  hsv(hues[index], saturations[index], values[index])
}

function renderCalc(index, x, y) {
  a = hypot(x-.5,y-.5)
  b = sin(x+y)
  c = cos(x-y)*(x+y)
  hues[index] = a
  saturations[index] = b
  values[index] = c
  
  //now that this pixel is cached, call out to the function that draws from cache
  //so the first frame isn't blank (or code duplicated)
  renderCache(index, x, y)
  
  if (index == pixelCount - 1) {
    render2D = renderCache //at the end of the last pixel, swap to the cache render
  }
}

export var render2D = renderCalc //start with the calc version of render
2 Likes

Oh that answers a few questions I didn’t ask yet…

Use multiple single arrays over nested ones, seems obvious, but still worthwhile point.

Good trick to create a one time loopthru/setup, cache or otherwise.

Just realized I was wondering (and testing seems to confirm, but…) we don’t have default values to a function, right?

You can’t do
function myFunction( x, y=1){}

Right?

What do unpassed variables get set to? 0 only?

And your last example, at 105fps, which is about 15% faster than the 90fps of the minimal one, is a nice confirmation that caching done right can help. On a slower pattern, it might even be the difference between usable and too slow, especially at higher pixel counts.

So coming up with a good example of how to cache like the above, it can be adapted when needed. For a pattern that is doing 60+ (or less?) fps without it, probably not a concern.

I can speak to doing this for default parameters:

function myFunction(x, y) {
  y = y || 1
}

Whether they come in as null or 0, I do know PB says both these are true: 0 == null, and 0 === null. Also, ||= is not a thing.

1 Like

Yep, no defaults for missing parameters, set to zero. Everything defaults to zero.

Pixelblaze doesn’t yet have a runtime type system, so presently there’s no difference between 0, null, or false. All values are numbers. In general I’d recommend writing compatible JavaScript like code where possible for compatibility and future language implementation.

I don’t believe === is valid PB syntax anyway.

Not having a clean way to set a default function value is awkward. So many things aren’t doable. Using zero as the sign of a default needed can work, except, of course, when zero is a valid value you want to use otherwise. I suppose you could fake it and make it handle some other value passed as zero, like this

function myFunction(x, y) {
  y = y || 1
  if (y == -32766) {y = 0}
}

But… Ugly.

Does eliminating a, b and c make any difference? They aren’t used for anything…

1 Like