3 ms·
I've kept openEtG's wasm blob (card game engine) <400kb while migrating a lot of logic (card text generation, etc) to wasm. It's Rust. Doing so takes some care:
by __s 3y ago
I've kept openEtG's wasm blob (card game engine) <400kb while migrating a lot of logic (card text generation, etc) to wasm. It's Rust. Doing so takes some care:
1. avoid floats (fixed point arithmetic saved quite a bit of space)
2. avoid hashmaps (originally used hashmaps since it was easy to port JS maps to, have since ported everything to vecs)
3. avoid strings (for awhile there were no strings, but eventually brought it in for display logic)
4. use a small allocator, like talc
5. avoid dependencies. I only use rand & fxhash. I should probably get rid of rand (fxhash only used to hash game state to check for desyncs)
6. avoid generic diversity. I try to keep a small set of instantiated types, for example Vec<i16> is there so no need to bring in Box<[i16]> or anything. Getting away from floats/hashmaps helped reduce type diversity
7. design algorithms with size in mind, I have a couple lookup tables where I pack bits https://github.com/serprex/openEtG/blob/2011007dec2616d1a24d3de6dd7cad93478711dd/src/rs/src/etg.rs#L12 https://github.com/serprex/openEtG/blob/2011007dec2616d1a24d... encodes an adrenaline mechanic where multiple attacks give lower attack power creatures more attacks than higher attack power creatures. Care was taken comparing how much decoding logic cost compared to storing unpacked values. AI evaluation uses 6 bit fixed precision because 64 encodes more efficiently than 128 in webassembly
Similarly there's a targeting mechanism with AND/OR & predicates. I used to have an AST like format with each predicate getting an enum & AND/OR being a slice of expressions. Now each expression is 32 bit integers encoding expression in polish notation, AND/OR have 2 bit codes & predicates are 6 bits (polish notation won over reverse polish here because with polish notation I was able to have AND/OR short circuit evaluation)
- 01HNNWZ0MV43FF 3y agoWow! Do you have numbers anywhere showing the space saved? Especially for step 6, using a Vec as a Box, I would not expect that to save much.
- __s 3y agoUnfortunately nothing concrete, it's a few kb here & there. I'd estimate I've saved ~300kb overall. I'd have to go back to compare these things. Removing floats saved more space than I expected Also important is configuring compiler correctly, [profile.release] opt-level = "z" lto = "fat" codegen-units = 1
- pdpi 3y ago> Especially for step 6, using a Vec as a Box, I would not expect that to save much. It's not that using a Vec as a Box saves anything at all. It's that generics require monomorphisation, and that eats up a bunch of space, and more instances of generic types translates into more space. If your application uses Vec<i16> elsewhere, you've already paid the binary size price for using it. Vec<i16> is close enough to Box<[i16]> that the functional differences don't warrant the extra code generation.
- nextaccountic 3y agohe meant Box<[T]> (aka fixed-size vec or heap allocated array) rather than Box<T> (a single heap allocated element)
- chongli 3y agoavoid floats (fixed point arithmetic saved quite a bit of space) Can someone help me understand how this works? I thought JavaScript uses doubles for everything. Is WASM completely different in this regard?
- colejohnson66 3y agoYes. WASM has proper integer and floating-point types.
- wongarsu 3y agoAnd even JavaScript only conceptually uses doubles everywhere, JavaScript engines do use integers where they get away with it. Your loop counter is almost certainly an int. You can use this to your advantage for optimization purposes if you carefully craft your statements to probably fit in integers (e.g. by adding bitwise operations)
- deleted 3y ago[deleted]
- flohofwoe 3y agoWASM has 32-bit and 64-bit integer and float types: https://webassembly.github.io/spec/core/syntax/types.html https://webassembly.github.io/spec/core/syntax/types.html (...and additionally can load/store 8- and 16-bit integers).
- WanderPanda 3y agoI've been working on a C++ deep RL library (RLtools) and also created some WASM examples (https://rl.tools https://rl.tools). I didn't pay any attention to the binary size at all but it turns out it is also just around 200-300kb (including everything, deep learning forward/backward, RL algo and dynamics simulation). Even though it's not prohibitive rn, I'm curious how small it could be and hopefully find some time soon to squeeze it down
- rowanG077 3y agoWhy do floats and strings have such a high cost?
- josephg 3y agoStrings seem to have a high cost because there's a lot of complexity in rust's format! macros. And the generated code seems to end up with a lot of obscure ways it can panic. I've found just having one stray dbg!() in my rust code can add ~20kb to my wasm bundle size. Look at what a single dbg!(some_u32) generates: https://rust.godbolt.org/z/bex9z8vx7 https://rust.godbolt.org/z/bex9z8vx7 . Its also calling into a bunch of garbled functions in the standard library - which will bring in a lot more code. I suspect zig's comptime approach might work much better for for this sort of thing, if we want smaller binary sizes.
- josephg 3y agoMore things that I've found helped: - Use wasm-opt from binaryen. That seems to reliably drop wasm size by ~20-30%. ( https://github.com/WebAssembly/binaryen https://github.com/WebAssembly/binaryen ) - Use brotli compression for serving wasm bundles to the browser, and make sure your web server is setup to use the brotli compressed files. (Its a 1 line change in nginx, for example). Brotli drops the size of wasm bundles by about 3x. Its much better than gzip for wasm.
- 38 3y agojesus christ, just do a desktop app. by your own admission you only saved 300k with all that. a desktop app at 700k is nothing, which means you could ignore all these optimizations and focus on improving speed or adding features
- flohofwoe 3y agoArguably, on the web a difference between 300 KB and 700 KB doesn't matter either, many web pages have a single hero image bigger than that. But for a long time one main critique point was that WASM apps are "bloated", so its good to see counter examples, and that devs start caring about trimming fat in general (this would also be a good thing for many desktop applications tbh).
- crabmusket 3y ago> 1. avoid floats (fixed point arithmetic saved quite a bit of space) This is really interesting, since WASM has built in float types of course - is there any more detail you can go into / examples you can show us? I have a problem at work where I was thinking fixed point arithmetic might help, because I know my maximum resolution needs (e.g. I know the problem will never care about sub-millimetre positioning). I'd be interested to hear more about related issues.
- dartos 3y agoThey said why right there. Floating point numbers take up more space in memory and can only store smaller values than internet.
- remexre 3y agoAre you sure they're not talking about code size? Not sure what limiting instantiations would have to do with heap size... (I also find it surprising that avoiding floats helps anything; maybe just in avoiding more instantiations? Because if the std floating point functions don't fully inline, you've got bigger problems than wasm blob size...)
- __s 3y agoI'm talking about code size. Wasm has efficient encodings for small integers, but constants for floats always take up 5/9 bytes
- crabmusket 3y agoOh I see! Was this the main benefit, not the use of fixed point algorithms?