Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add tagged lightuserdata #1087

Merged
merged 17 commits into from
Dec 14, 2023

Conversation

petrihakkinen
Copy link
Contributor

This change adds support for tagged lightuserdata and optional custom typenames for lightuserdata.

Background: Lightuserdata is an efficient representation for many kinds of unmanaged handles and resources in a game engine. However, currently the VM only supports one kind of lightuserdata, which makes it problematic in practice. For example, it's not possible to distinguish between different kinds of lightuserdata in Lua bindings, which can lead to unsafe practices and even crashes when a wrong kind of lightuserdata is passed to a binding function. Tagged lightuserdata work similarly to tagged userdata, i.e. they allow checking the tag quickly using lua_tolightuserdatatagged (or lua_lightuserdatatag).

The tag is stored in the 'extra' field of TValue so it will add no cost to the (untagged) lightuserdata type.

Alternatives would be to use full userdata values or use bitpacking to embed type information into lightuserdata on application level. Unfortunately these options are not that great in practice: full userdata have major performance implications and bitpacking fails in cases where full 64 bits are already used (e.g. pointers or 64-bit hashes).

Lightuserdata names are not strictly necessary but they are rather convenient when debugging Lua code. More precise error messages and tostring returning more specific typename are useful to have in practice (e.g. "resource" or "entity" instead of the more generic "userdata").

Impl note: I did not add support for renaming tags in lua_setlightuserdataname as I'm not sure if it's possible to free fixed strings. If it's simple enough, maybe we should allow renaming (although I can't think of a specific need for it)?

@HaroldCindy
Copy link
Contributor

HaroldCindy commented Nov 5, 2023

I'd also be interested in this being implemented. I'm serializing the state of Threads, and it's difficult for me to distinguish the lightuserdatas Luau uses internally to track iterator state, and lightuserdatas owned by user code.

Question though, should the tag affect lightuserdatas hashing and equality? For example, if I have an empty Table and I set keys for both lightuserdata(0, 0) and lightuserdata(0, 1), should that Table have 2 keys or 1? IMO the tag should be taken into consideration, but I don't know how that would affect existing Luau consumers.

I also don't know about keeping track of lightuserdata tag names, seems like that's something the Luau consumer should keep track of itself, and they can customize tostring if they want.

@petrihakkinen
Copy link
Contributor Author

Good points!

We also use lightuserdata for custom iterator state, so that would indeed be another use case for tags.

Question though, should the tag affect lightuserdatas hashing and equality?

You're right, that is clearly an omission. I added tag checks to all the equality checks. However, I'm not sure if hashing the tags is necessary. I'm trying to think how often we would use lightuserdata with same value but different tag as a table key. Note that hash collisions only makes table operations slightly slower in the worst case (there should be no other observable effects). If these cases are rare, maybe it would not make sense to add overhead to lightuserdata hashing?

I also don't know about keeping track of lightuserdata tag names

Tag names are used by the VM when printing or when converting lightuseradata to string with tostring. Normally we use metatables to configure typenames but since lightuserdata values are not objects, they share the same metatable.

Alternatively we could replace lua_setlightuserdataname with more general lua_setlightuserdatametatable(tag, mt) API, especially if there's need to further customize lightuserdata subtypes? I'm also ok with dropping lightuserdata names in the this first version if this feels too messy.

@HaroldCindy
Copy link
Contributor

I added tag checks to all the equality checks. [...] Note that hash collisions only makes table operations slightly slower in the worst case (there should be no other observable effects). If these cases are rare, maybe it would not make sense to add overhead to lightuserdata hashing?

Makes sense to me, thanks! Looks like this should work correctly in JITed code as well, since that will still use luaV_equalval for lightuserdata.

Alternatively we could replace lua_setlightuserdataname with more general lua_setlightuserdatametatable(tag, mt) API, especially if there's need to further customize lightuserdata subtypes?

I don't feel too strongly. If it was a choice between names and metatables, I'd take names as you've implemented them here.

@zeux
Copy link
Collaborator

zeux commented Nov 7, 2023

distinguish the lightuserdatas Luau uses internally to track iterator state

Note that as implemented, this PR introduces the notion of "untagged" light user data in that not all light user data objects have tags (specifically, those produced by the VM code don't). I think we'll need to fix that, maybe we could then reserve a tag for internal iterator state. However this also caused us to look at some internal code that manages the iterator state and that code has some issues that we'd need to fix before adding tags. There's some perf cost for setting tags up so we'd need to be careful but it can probably be managed if we only initialize them when the loop is set up.

The access code would need to be more careful. We shouldn't access extra directly; ideally setpvalue would always set up the tag (by requiring it as a parameter), the tag would be accessed by a new macro similar to pvalue, and then we can figure something out for updating loop state without writing the tag if that ends up being necessary for perf.

I would recommend avoiding metatable customization at least for now; it can be useful, but it's also not free as it will impact every place where type -> metatable lookup is performed by adding more branches. Compared to that, names are basically free.

Also cc @vegorov-rbx for further comments if any.

@petrihakkinen
Copy link
Contributor Author

Thanks! Pushed new revision:

  • setpvalue now sets the tag
  • lightuserdatatag macro reads the tag
  • internal iterators now use reserved internal tag LU_TAG_ITERATOR, which is beyond the range of user specified tags
  • added setpvaluefast, a variant of setpvalue which only sets the value (fast path for iterators)

However, there's an issue which I don't understand. For some reason LOP_FORGPREP_INEXT and LOP_FORGPREP_NEXT in the VM and forgLoopNodeIter cause assertion failures if I change them to use setpvaluefast, so I use the slower setpvalue for them instead. Does this perhaps happen because iterator state is not set fully at the start of loops? I'd need some help here.

Alternatively we could drop setpvaluefast or leave it for future work; I doubt it is a significant win in practice, since the same cache line is being modified anyway when setting the value.

@zeux
Copy link
Collaborator

zeux commented Nov 8, 2023

I would start with just using setpvalue. We will need to thoroughly analyze the performance impact here anyway, and it's easier to do this from a simpler baseline. As for "why", this is what I was referring to above wrt issues in codegen: codegen actually never sets up the lightuserdata properly in X64 because it fully inlines the loop prologue in the optimistic case, which only writes the lightuserdata value partially and doesn't write the tag at all. We are looking into a better way to structure this to avoid these issues in the future.

@petrihakkinen
Copy link
Contributor Author

Alright, done. Please let me know if there's anything else.

@petrihakkinen
Copy link
Contributor Author

I finally had time to benchmark the changes. I first ran the regular tests in bench/tests but any differences fell under the noise threshold. So next I ran a few selected micro tests which stress-test looping (test_LargeTableSum*.lua). I disabled garbage collection for the tests to further reduce noise. Conclusion: there doesn't seem to be any measurable speed difference, at least on my hardware (i7-9700K).

@zeux, have you had time to think about this yet? I believe this change would make lightuserdata more useful generally. The problem is, at the moment an application can only use lightuserdata values safely for a single purpose, unless tags are encoded manually and directly into the 64-bit lightuserdata values which is problematic in many ways. (FWIW, we are very interested in this because we're heavily using lightuserdata and we'd like to have water tight type checks in our bindings.)

Command line used:
python bench.py --vm luau-unmodified.exe --compare luau-lu-tags.exe --folder micro_tests --run-test test_LargeTableSum* --extra-loops 10

Note that I added collectgarbage("stop") and collectgarbage("restart") calls before and after each test (these require modified executables).

Results:

==================================================RESULTS==================================================
Test                              | Min         | Average     | StdDev%    | Driver               | Speedup    | Significance   | P(T<=t)
----------------------------------|-------------|-------------|------------|----------------------|------------|----------------|---------
LargeTableSum: for i=1,#t         |    8.274ms  |    8.447ms  |    0.329%  | luau-unmodified.exe  |            |                |
LargeTableSum: for i=1,#t         |    8.187ms  |    8.296ms  |    0.173%  | luau-lu-tags.exe     |    1.827%  | likely better  |      0%
LargeTableSum: for k,v in ipairs  |    6.587ms  |    6.645ms  |    0.133%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in ipairs  |    6.586ms  |    6.663ms  |    0.159%  | luau-lu-tags.exe     |   -0.272%  | likely worse   |      1%
LargeTableSum: for k,v in {}      |    6.343ms  |    6.417ms  |    0.152%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in {}      |    6.339ms  |    6.396ms  |    0.131%  | luau-lu-tags.exe     |    0.329%  | likely better  |      0%
LargeTableSum: for k,v in pairs   |    6.339ms  |    6.409ms  |    0.156%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in pairs   |    6.338ms  |    6.404ms  |    0.177%  | luau-lu-tags.exe     |    0.079%  | likely same    |     51%
Total                             |   27.543ms  |   27.918ms  |       ---  | luau-unmodified.exe  |            |                |
Total                             |   27.450ms  |   27.758ms  |       ---  | luau-lu-tags.exe     |    0.575%  |                |
---
'luau-lu-tags.exe' change is 0.488% positive on average

==================================================RESULTS==================================================
Test                              | Min         | Average     | StdDev%    | Driver               | Speedup    | Significance   | P(T<=t)
----------------------------------|-------------|-------------|------------|----------------------|------------|----------------|---------
LargeTableSum: for i=1,#t         |    8.267ms  |    8.388ms  |    0.164%  | luau-unmodified.exe  |            |                |
LargeTableSum: for i=1,#t         |    8.186ms  |    8.278ms  |    0.157%  | luau-lu-tags.exe     |    1.326%  | likely better  |      0%
LargeTableSum: for k,v in ipairs  |    6.582ms  |    6.668ms  |    0.176%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in ipairs  |    6.588ms  |    6.671ms  |    0.181%  | luau-lu-tags.exe     |   -0.043%  | likely same    |     74%
LargeTableSum: for k,v in {}      |    6.344ms  |    6.445ms  |    0.199%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in {}      |    6.338ms  |    6.428ms  |    0.190%  | luau-lu-tags.exe     |    0.253%  | likely same    |      7%
LargeTableSum: for k,v in pairs   |    6.344ms  |    6.440ms  |    0.195%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in pairs   |    6.339ms  |    6.433ms  |    0.196%  | luau-lu-tags.exe     |    0.097%  | likely same    |     49%
Total                             |   27.537ms  |   27.940ms  |       ---  | luau-unmodified.exe  |            |                |
Total                             |   27.452ms  |   27.810ms  |       ---  | luau-lu-tags.exe     |    0.465%  |                |
---
'luau-lu-tags.exe' change is 0.407% positive on average

==================================================RESULTS==================================================
Test                              | Min         | Average     | StdDev%    | Driver               | Speedup    | Significance   | P(T<=t)
----------------------------------|-------------|-------------|------------|----------------------|------------|----------------|---------
LargeTableSum: for i=1,#t         |    8.271ms  |    8.378ms  |    0.153%  | luau-unmodified.exe  |            |                |
LargeTableSum: for i=1,#t         |    8.188ms  |    8.335ms  |    0.275%  | luau-lu-tags.exe     |    0.525%  | likely better  |      0%
LargeTableSum: for k,v in ipairs  |    6.587ms  |    6.669ms  |    0.181%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in ipairs  |    6.589ms  |    6.669ms  |    0.183%  | luau-lu-tags.exe     |    0.009%  | likely same    |     95%
LargeTableSum: for k,v in {}      |    6.344ms  |    6.428ms  |    0.191%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in {}      |    6.339ms  |    6.421ms  |    0.186%  | luau-lu-tags.exe     |    0.114%  | likely same    |     40%
LargeTableSum: for k,v in pairs   |    6.342ms  |    6.426ms  |    0.188%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in pairs   |    6.340ms  |    6.445ms  |    0.202%  | luau-lu-tags.exe     |   -0.290%  | likely worse   |      4%
Total                             |   27.544ms  |   27.902ms  |       ---  | luau-unmodified.exe  |            |                |
Total                             |   27.456ms  |   27.869ms  |       ---  | luau-lu-tags.exe     |    0.119%  |                |
---
'luau-lu-tags.exe' change is 0.089% positive on average

==================================================RESULTS==================================================
Test                              | Min         | Average     | StdDev%    | Driver               | Speedup    | Significance   | P(T<=t)
----------------------------------|-------------|-------------|------------|----------------------|------------|----------------|---------
LargeTableSum: for i=1,#t         |    8.268ms  |    8.369ms  |    0.145%  | luau-unmodified.exe  |            |                |
LargeTableSum: for i=1,#t         |    8.186ms  |    8.286ms  |    0.159%  | luau-lu-tags.exe     |    1.000%  | likely better  |      0%
LargeTableSum: for k,v in ipairs  |    6.584ms  |    6.660ms  |    0.158%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in ipairs  |    6.589ms  |    6.662ms  |    0.168%  | luau-lu-tags.exe     |   -0.016%  | likely same    |     89%
LargeTableSum: for k,v in {}      |    6.344ms  |    6.427ms  |    0.184%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in {}      |    6.338ms  |    6.415ms  |    0.182%  | luau-lu-tags.exe     |    0.191%  | likely same    |     15%
LargeTableSum: for k,v in pairs   |    6.341ms  |    6.422ms  |    0.188%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in pairs   |    6.338ms  |    6.413ms  |    0.167%  | luau-lu-tags.exe     |    0.145%  | likely same    |     26%
Total                             |   27.537ms  |   27.879ms  |       ---  | luau-unmodified.exe  |            |                |
Total                             |   27.451ms  |   27.776ms  |       ---  | luau-lu-tags.exe     |    0.372%  |                |
---
'luau-lu-tags.exe' change is 0.329% positive on average

==================================================RESULTS==================================================
Test                              | Min         | Average     | StdDev%    | Driver               | Speedup    | Significance   | P(T<=t)
----------------------------------|-------------|-------------|------------|----------------------|------------|----------------|---------
LargeTableSum: for i=1,#t         |    8.275ms  |    8.404ms  |    0.180%  | luau-unmodified.exe  |            |                |
LargeTableSum: for i=1,#t         |    8.188ms  |    8.378ms  |    0.454%  | luau-lu-tags.exe     |    0.314%  | likely same    |     21%
LargeTableSum: for k,v in ipairs  |    6.587ms  |    6.718ms  |    0.403%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in ipairs  |    6.587ms  |    6.664ms  |    0.163%  | luau-lu-tags.exe     |    0.819%  | likely better  |      0%
LargeTableSum: for k,v in {}      |    6.344ms  |    6.414ms  |    0.171%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in {}      |    6.339ms  |    6.409ms  |    0.168%  | luau-lu-tags.exe     |    0.074%  | likely same    |     54%
LargeTableSum: for k,v in pairs   |    6.347ms  |    6.501ms  |    0.466%  | luau-unmodified.exe  |            |                |
LargeTableSum: for k,v in pairs   |    6.340ms  |    6.414ms  |    0.178%  | luau-lu-tags.exe     |    1.362%  | likely better  |      0%
Total                             |   27.553ms  |   28.038ms  |       ---  | luau-unmodified.exe  |            |                |
Total                             |   27.454ms  |   27.865ms  |       ---  | luau-lu-tags.exe     |    0.621%  |                |
---
'luau-lu-tags.exe' change is 0.641% positive on average

@HaroldCindy
Copy link
Contributor

For the record, I've been using this for the last week or so with no noticeable perf reduction, and it's heavily reduced the number of cases where I need to use GC'd userdatas.

@zeux
Copy link
Collaborator

zeux commented Nov 28, 2023

I'll let @vegorov-rbx review this and make the call regarding the value + note if further code adjustments would be necessary; from my perspective this is a good change that increases usefulness and safety of lightuserdata without significant downsides. Native codegen will need to be adjusted in the future for correctness as it doesn't currently write tags and that would need new IR instructions, but that can probably be done later / separately. Thanks for running the micro benchmarks - based on these and on the future possible improvements (like elidining tag or even type tag writes in the iteration opcode) I am not concerned about performance impact here.

@petrihakkinen
Copy link
Contributor Author

Thanks!

One thought that came up today: currently internal tags are >= LUA_LU_TAG_LIMIT. Would it be cleaner to reserve negative values for internal tags instead? That way LU_TAG_ITERATOR would be equal to -1 and we could remove #define LU_TAG_COUNT (LU_TAG_ITERATOR+1) from lobject.h. This might be slightly cleaner, perhaps less surprising and more in line with things like pseudo stack indices. Thoughts?

@zeux
Copy link
Collaborator

zeux commented Nov 28, 2023

Note that we are using a similar structure for regular user data tags, with reserved values for newproxy and inline dtor, so I would keep the numbering the same - maybe there’s an argument to rework both (later/separately) but I don’t think it matters a lot one way or the other.

@petrihakkinen
Copy link
Contributor Author

Ok check, let's keep the tags consistent with regular userdata.

@petrihakkinen
Copy link
Contributor Author

Hi @vegorov-rbx! Would you have time to review this pull request?

VM/src/lstate.h Outdated Show resolved Hide resolved
VM/src/lobject.cpp Outdated Show resolved Hide resolved
VM/src/lobject.cpp Outdated Show resolved Hide resolved
VM/src/ltm.cpp Outdated Show resolved Hide resolved
VM/src/lvmexecute.cpp Outdated Show resolved Hide resolved
VM/src/lvmexecute.cpp Outdated Show resolved Hide resolved
VM/src/lvmutils.cpp Outdated Show resolved Hide resolved
@petrihakkinen
Copy link
Contributor Author

Thank you for the review! Pushed the changes.

@vegorov-rbx vegorov-rbx merged commit 2173938 into luau-lang:master Dec 14, 2023
7 checks passed
@vegorov-rbx
Copy link
Collaborator

Thank you!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Development

Successfully merging this pull request may close these issues.

4 participants