BufferEncoder - Very efficient table to buffer serializer that doesn’t use schemas

BufferEncoder

Github | Wally | Model

BufferEncoder is a table to buffer serializer that is designed to be as performant as possible while being able to serialize any type of table you want with all datatypes realistically used in tables.
It automatically serializes the table for you without having to define a schema for how the table is serialized - unlike most other serializers publicly available right now - and has very simple API to use.

Features

  • Very optimized and space-efficient
  • Supports any type of table (array, dictionary, mixed), cyclic tables, and any value for keys in dictionaries
  • Supports encoding every datatype that you’d realistically put in a table
  • Can encode custom values into 2 bytes if registered beforehand using encoder.enums.register() (eg. userdata from newproxy())
  • Can deduplicate repeated tables, strings, numbers, vectors, and enumitems to reduce data size
  • Has syncing from server to client for networking purposes
  • Has simple encryption that relies on psuedo-random number generation
  • Fully typed

Performance

It performs very fast compared to all serializers I’ve found publicly available
It may in some cases be faster than JSONEncode and JSONDecode, depending on the structure of your data

Benchmark

i was only able to find one module that works similar to mine, benchmarking with ones that use schemas isn’t really viable since they dont work the same as mine :expressionless:

Comparison between BufferEncoder, MessagePack, and HTTPService (native is enabled for both modules)

Code
--!optimize 2
--!native

local HTTP = game:GetService("HttpService")
local ReplicatedStorage = game:GetService("ReplicatedStorage")
local benchmarkmodules = ReplicatedStorage.benchmarkmodules

local BufferEncoder = require(ReplicatedStorage.BufferEncoder:Clone())
local MessagePack = require(benchmarkmodules.msgpack:Clone())

local c = 50

local tab, tab2, tab3, tab4, tab5, tab6 = {}, {}, {}, {}, {}, {}
for i = 1, c do tab[i] = {i, tostring(i), string.rep("a", i), true, false, nil, 4, "ok", true, 5436} end

for i = 1, c do tab2[i] = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" end

for i = 1, c do tab3[i] = i ^ 2 end

for i = 1, c do tab4[i] = {a = 'b', b = 'c', c = 'd', e = {a = 1, b = 2, c = "ok"}} end

for i = 1, c do 
	tab5[i] = {math.random(1, 1000), math.random(1, 1000), math.random(1, 1000), math.random(1, 1000), math.random(1, 1000), math.random(1, 1000), math.random(1, 1000), math.random(1, 1000), math.random(1, 1000)}
end

for i = 1, c do tab6[tostring(i)] = tostring(i) end

local tabs = {
	tab,
	tab2,
	tab3,
	tab4,
	tab5,
	tab6
}

return {
	ParameterGenerator = function()
		return BufferEncoder, MessagePack, HTTP, tabs
	end,

	Functions = {
		["BufferEncoder"] = function(Profiler, BufferEncoder, MessagePack, HTTP, t) 
			local r = BufferEncoder.write(t, nil, nil, nil, nil, Profiler)
			Profiler.Begin("Read")
			BufferEncoder.read(r)
			Profiler.End()
		end,
		
		["JSONEncode & Decode"] = function(Profiler, BufferEncoder, MessagePack, HTTP, t) 
			Profiler.Begin("Write")
			local r = HTTP:JSONEncode(t)
			Profiler.End()
			Profiler.Begin("Read")
			HTTP:JSONDecode(r)
			Profiler.End()
		end,

		["MessagePack"] = function(Profiler, BufferEncoder, MessagePack, HTTP, t) 
			Profiler.Begin("Write")
			local r = MessagePack.encode(t)
			Profiler.End()
			Profiler.Begin("Read")
			MessagePack.decode(r)
			Profiler.End()
		end,
	},
}

Installation

You can install it on Wally or from the github page, Links are at the top of the post.

Basic Example

API can be found in the github repository

local BufferEncoder = require(path_to_encoder)
local parts = {}
local Properties = {'Name', 'ClassName', 'CFrame', 'Color', 'Transparency', 'Material', 'Anchored'}
for _, part in workspace:GetDescendants() do 
   if part:IsA("BasePart") then
      local t = {}
      for _, prop in Properties do t[prop] = part[prop] end
      table.insert(parts, t)
   end
end

local buff = BufferEncoder.write(parts)
-- you could then save that buffer or send it in a remote or whatever you want

Credit

6 Likes

Version 1.2.0

  • Deduplication was remade so it doesn’t require writing to iterate over table content twice
    This increases writing performance by ~20% when deduplication is enabled
    Size of buffer is a little smaller since a separate table containing the deduplicated values is no longer needed.

  • Writing in general is faster, especially if there’s many subtables.

  • Added allowdeduplication parameter to encoder.read(), This should be enabled if the buffer had deduplication enabled when writing.

  • Uploaded the module to toolbox, so you can get it from there now.

Benchmark

deduplication is enabled

Code
--!native

-- yeah this is the basic example thats used in the post lol
local parts = {}
local Properties = {'Name', 'ClassName', 'CFrame', 'Color', 'Transparency', 'Material', 'Anchored'}
for _, part in workspace:GetDescendants() do 
	if part:IsA("BasePart") then
		for i = 1, 150 do 
			local t = {}
			for _, prop in Properties do t[prop] = part[prop] end
			t.silly = 534
			table.insert(parts, t)
		end
	end
end


local v110, v120 = require(game.ReplicatedStorage.benchmarkmodules["BufferEncoderV1.1.0"]:Clone()), 
	require(game.ReplicatedStorage.BufferEncoder:Clone())

return {
	ParameterGenerator = function()
		return v110, v120, parts, 1, nil
	end,

	Functions = {
		["BufferEncoderV1.1.0"] = function(Profiler, v110, v120, t, step, shift)
			v110.write(t, nil, nil, true, nil, Profiler)
		end,

		["BufferEncoderV1.2.0"] = function(Profiler, v110, v120, t, step, shift)
			v120.write(t, nil, nil, true, nil, Profiler)
		end,
	},
}
2 Likes

One quick question. Does not having a schema slow down the process? Compared to BufferConverter2 or Remote for example.

It is slightly slower yeah, for this module though it is pretty fast for the things I benchmarked so I say it doesn’t really matter unless you’re running in a tight loop with lots of serialization and deserialization calls

require(game.ReplicatedStorage.BufferEncoder):write({[1] = 12})

  10:39:19.650  ReplicatedStorage.BufferEncoder.Write:90: attempt to perform arithmetic (add) on table and number  -  Edit - Write:90
  10:39:19.650  Stack Begin  -  Studio
  10:39:19.650  Script 'ReplicatedStorage.BufferEncoder.Write', Line 90 - function write  -  Studio - Write:90
  10:39:19.650  Script 'ReplicatedStorage.BufferEncoder.Write', Line 696  -  Studio - Write:696
  10:39:19.650  Script 'require(game.ReplicatedStorage.BufferEncoder):write({[1] = 12})', Line 1  -  Studio
  10:39:19.650  Stack End  -  Studio

Can table’s not be encoded? Or am I missing something?

It’s .write not :write() //30charlimit//

1 Like

xD
Not sure how I missed that, thanks a lot.

Little question. I’ve gotten everything working but it seems reading part datatypes is returning nil / empty tables.

local mod = require(game.ReplicatedStorage.Frame.PKGS.BufferEncoder)

local cBuff = mod.write({[1] = game.Workspace.TestPart})
print(cBuff) -- buffer: 0xd55fed9b1ff9d46b

local vRead = mod.read(cBuff)
print(vRead) -- {}

Assuming I’d need to allow references? And then on read put the table of references it can read, no?

Yeah, you need to enable allowreferences and use the table returned from write in encoder.read() for instances to work.

Curious how you got such different numbers on msgpack library.

Did you take the latest source from github repository which uses byte buffers?
Did you try adding the same optimisation hint to the module?

I used the latest version from the repository, though I hadn’t added --!native at start so that’s the reason why the numbers came out that way. my bad, I’ll correct the first post.

I added --!native and the result was around 2.5x faster for msgpack


This is the same benchmark code as in the first post (without httpservice)

1 Like

I want to use this for my RemoteEvent module (Network) and want buffers to be a built-in behavior for sending/receiving and automatically encode/decode etc

how does this Module handle Instances, I’m assuming that would error since it’s not a supported type

Instances are saved in a separate table that you send alongside the buffer in the remote. You have to enable ‘allowreferences’ in order for them to be saved, though.

All network modules do something similar to this, since there isn’t a better option at the moment.

I’m thinking of adding built-in sanitization for NaN and math.huge for numbers and all datatypes. This converts nan and math.huge into 0 if encountered.

Would this be useful for you?
  • Yes
  • No - I prefer to sanitize them myself.
  • No - It doesn’t matter if it exists in the buffer for me.

0 voters

It will be optional - toggleable by settings module - and will operate only during buffer decoding, so sanitization would work for previously saved buffers and bypassing isn’t possible.