Luau read-only tables [proxy vs. freeze] and which is more efficient (research included)

Luau Table Immutability Applied Research

The concept of immutability is one that should be familiar to most seasoned programmers, it implies that a specific object’s state cannot change past the moment where it is created. Lua Users Wiki, an unofficial popular Lua context website writes, “Lua tables are mutable, but strict immutability can be simulated to some extent (bar rawset) via metamethods …” (2016, p. 1). This is important to understand when it comes to Lua (and Luau), and is the foundation for this entire paper. As of November 2021, Roblox has “formalized” table immutability within Luau under the function table.freeze, which has been placed inside Luau’s modified table library. “WheretIB” (Roblox staff) writes about table.freeze under its initial release, “Note that the table is frozen in-place and is not being copied. Additionally, only t is frozen, and keys/values/metatable of t don’t change their state and need to be frozen separately if desired” (2021, p.1). This functionality describes the exact distinction that this paper intends to explore. A truly immutable table object should not refer to a proxy table, where the original table pretends to be immutable (this would mean “copying”/forcing immutability regardless of performance). Conceptually, with no prior research or testing, it feels safe to assume that table.freeze would outperform a proxy table - right? A proxy table and a metatable should theoretically be outperformed by table.freeze in almost every situation. The provided research in this paper is meant to answer this question, as well as provide certain ground rules when working with table.freeze moving forward.

Foundation
It is important we set rules for what will be considered immutable and be talked about in this paper (because Luau is an abstract and interesting language with clear deviations from other scripting languages). Unofficial immutability through metatables and proxies will be considered immutable in this paper, even though all of these objects are mutable. This is because to have a fair comparison between the two we need to allow ourselves to think in terms of performance instead of actual syntactic definitions. The research inside of this paper has been conducted inside of Roblox studio and does not stand as applicable to any other IDE that supports Luau. This research could be nullified in the future if table.freeze sees performance improvements or implementation alterations.

In this paper we will refer to two types of memory objects, one which will be called “copy-type” objects and one which will be called “complex-type” objects. Copy-type objects will refer to objects that exist in memory without a specified identity as far as Lua and Luau are concerned. This will be strings, numbers, booleans, and “nil” objects. Complex-type objects will refer to objects that hold identity and have uniqueness to Lua and Luau. This will be tables, threads, and functions.

Figure 1

An example of copy-type objects shown with Lua code


Note. This figure demonstrates how copy-type objects do not hold identity, and when referred to by a new reference are copied, pointing to a different memory address (to the scripter).

The comparison that this paper uses involving metatables and proxy tables assumes three complex-type objects. One table which will act as the proxy, one metatable which points to our read-only table, and one table which will simulate immutability through __index, __newindex, and __metatable.

Figure 2

What this paper refers to when talking about simulated immutability with proxy tables


Note. This figure shows how to use metamethods to simulate immutability to a certain extent in traditional Lua. This is what will be running against table.freeze in this paper.

There are inherent flaws with this paradigm that stem from Lua’s lackluster ability to cater to privacy in the way that other languages do. We will be ignoring these flaws for the sake of learning in this paper, and because as a Lua scripter, you should not need much privacy outside of packages. The references Readonly and Meta are both accessible, but they don’t need to be accessed, and they will not be. This paper will also not talk about newproxy. Newproxy is valuable in the conversation of immutability in that it can bar the usability of rawset and rawget. Newproxy has no place in this paper as our goal is to compare table.freeze to its counterpart in table-type objects.

The Research
The first thing to explore was a contradiction that was created between two differing sources that talk about table.freeze. Under the release thread for table.freeze, zeuxcg writes, “… for now I would not recommend making performance-related assumptions here” (2021, p.1). However, in the table.freeze GitHub documentation, zeuxcg writes about the proxy method, “… and carries a performance cost - both for creating the table, and for repeated property access” (2021, p.1). He is assumably correct in both sections, and functionality has improved over time. There is an innate cost to the proxy method because it carries three complex-type objects in memory. The first test was to put to rest which is more efficient in a traditional situation.

Figure 3

A test of computation speed for the proxy method versus table.freeze method in their actual code assignments


Note. This figure shows the process which was run to benchmark both concepts against one another. This test was run one hundred times to gather results.

Figure 4

This figure shows the results of the previously shown test when run one hundred times


Note. When the test was run one hundred times the freeze method outperformed the proxy method by a minimal margin.

It was interesting to see these results as they were unexpected. The proxy method puts more pressure on the code stack and also requires setmetatable, albeit a simple function, to run. The proxy method keeps pace with table.freeze in this situation; but, table.freeze wins consistently enough that it would be chosen for usage over the proxy method in every single situation where all that is required is the immutable table’s creation and functionality. In this instance freeze also makes more sense because less complex-type objects occupy memory. This will be a recurring concept in the shown research, where the table.freeze method is given favor because of the obvious benefit of it containing less stack pressure.

The next important test was how they each perform when accessing their indices. Each method was given a single table index to benchmark access of. By default you would expect table.freeze to excessively outperform the proxy method here. That is because Luau runs a number of statements when metatables exist, whereas table.freeze holds its own information and knows that it is frozen. The cost of indexing would be theorized as lesser for table.freeze.

Figure 5

This figure shows the code that was run to test indexing within these two methods. This was tested two hundred times.

Figure 6

This figure shows the results of the previous code when run against each other two hundred times.

As expected, table.freeze does infinitely better than the proxy method when it comes to reading indices. This test goes to show how miraculous some of the Luau changes are. There is a very clear answer here, and as far as indexing is concerned you should always be using table.freeze. There were eleven wins for the proxy method, which is negligible enough that it simply does not matter.

Conclusion
This research points to table.freeze being better at table immutability than a more traditional Lua read-only table in every sense possible. Moving forward, I will not be using proxy tables outside of when I feel that I would need to also unfreeze a table. For efficiency, you should do the same. Newproxy and table.freeze should be able to solve most situations where immutability is required at a fraction of the computation cost.


References

/ lua-users wiki: Immutable Objects. (n.d.). Lua Users Wiki. Retrieved December 4 /

/ Roblox. (2021, November 1). Luau Recap: October 2021. DevForum | Roblox. Retrieved December 4, 2022 /

/ Roblox. (n.d.). luau/function-table-freeze.md at master · Roblox/luau. GitHub. Retrieved December 4, 2022 /

24 Likes

wow… its useful, I thought its a old LUA system, but is a new one, and there is a tutorial you made! its so helpfull!

2 Likes