BrickColor.new() can take over a full millisecond

Hello! I have a side project I tinker with where I run automated benchmarks on roblox luau snippets (currently mostly low-level engine APIs) in order to find interesting optimization techniques for developers. If that sounds interesting to you, I also created an interactive dashboard for people to play with using the data - check it out here! Most of the stuff I’ve found so far is in the 30-40% improvement zone - nice, but not exactly a bug. That is - until I benchmarked BrickColor.new().

Using BrickColor.new() can take over 10x longer than Color3.new(), which feels very unintuitive as it’s an inferior datatype in most ways. However it’s used in various instance properties to this day. The most notable one being SelectionBox and SelectionSphere, which I and others often use for debugging and selection UX mechanics - notably, systems that frequently redraw each frame. When an API like this takes longer than a millisecond, if you change just a few colors in a single frame you’re dropping below 60 fps on the CPU side.

This was measured with 50 sequential runs of the luau execution open cloud API. The value was acquired by calling the method 50,000 with the same parameters across 5,000 different parameters. It has a surprisingly large standard deviation, and the actual benchmark distribution seems to imply the typical call taking around 0.5ms, with some lagging afterwards for unknown reasons.

I don’t have data published for running this API on a non luau-execuction server device, but I can verify from my own experience running the benchmark in dev mode on my local machine had it lagging far more than the others.

Let me know if you have any questions, thank you!

4 Likes

Very interesting to know but not surprising since BrickColor is very old and legacy from like 2010 lol.

2 Likes

Ha yeah, I wasn’t shocked by it being slower, but rather the magnitude - I was more just expecting something like what I saw with the Vector2 vs Vector3 situation.


They’ve clearly optimized Vector3s because they’re used so frequently, but Vector2s aren’t absurdly slow by any means.

1 Like

BrickColor.new(r, g, b) tries to map the color to the closest color in the BrickColor palette.
This requires looping over all available colors which is around 200.

BrickColor.new() does the same against 0, 0, 0 (it could be better, but unlikely to be prioritized).

BrickColor.new() being 10-20x slower than Color3.new() is not really unexpected given that it has to search for the color value most closely matching the input color.

What is unexpected is the results from your benchmark, because BrickColor.new() does not take nearly that much time to execute, it actually takes about 1.5μs per call, and Color3.new() is also much faster than these results imply at about 0.07μs per call. This is when feeding them completely random numbers for the color channels, so I’m not sure how this could possibly happen unless either Open Cloud execution is somehow 500x slower than my PC or your results are measured in total time rather than per-call time.

1 Like

Makes sense, thank you for letting me know - I initially assumed there had to be a faster way, but after exploring the problem a bit out of curiosity the current method of just looping through every one of them is almost certainly the fastest.

I guess I was assuming some colors would cluster in a manner which would allow for more shortcuts, but on closer inspection the boundaries are more irregular than I guessed. The only way which would probably be faster would be a giant look-up table, but I’m not sure speeding this up is worth using up an extra 20mb of memory lol.

Anyways, thanks again!

BrickColor.new() being 10-20x slower than Color3.new() is not really unexpected given that it has to search for the color value most closely matching the input color.

Yeah, I was assuming there would just be more shortcuts available but after exploring the problem further it’s probably already set up as well as it can be.

What is unexpected is the results from your benchmark, because BrickColor.new() does not take nearly that much time to execute, it actually takes about 1.5μs per call, and Color3.new() is also much faster than these results imply at about 0.07μs per call. This is when feeding them completely random numbers for the color channels, so I’m not sure how this could possibly happen unless either Open Cloud execution is somehow 500x slower than my PC or your results are measured in total time rather than per-call time.

For the most part my focus has been making sure the results don’t change between runs, allowing for comparison between tests. Part of the reason I have to run a single benchmark across 50 runs is because I’ve noticed an absurd amount of variation in the actual baseline speeds (measured with an empty benchmark) of the servers. The data has to go through a lot of cleaning as a result.

That being said - 500x is a bit much, I can confirm my own higher end pc when running the following comes in at around 0.037μs

local start = tick()
local amount = 1000000
for i=1, amount do
	Color3.new(123, 24, 10)
end
local stop = tick()
print(`{1000*1000*(stop-start)/amount}`)

This is still an early project, maybe I’m rounding from nanoseconds to μs twice somewhere in the power-bi import for the visualization? As 68 / 1000 = 0.068, that seems pretty likely. Thankfully, if that is the case it means the data itself is fine, and I just suck at Power BI lol

I’ll poke around and figure out where that wire is being crossed, thank you for noticing!

UPDATE: in the last version I changed to measure in picoseconds, rather than nanoseconds to cleanly store things as integers and forgot to update the labels / transform the rounding - the above values are actually in nanoseconds, not microseconds

UPDATE 2: the dashboards have been fixed

1 Like

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.