Roblox will automatically encode buffers using zbase64 which is 1000x faster because the backend is done in c++
--!native
--!optimize 2
local HttpService = game:GetService("HttpService")
local Offset = 34
local function Encode(Text)
local Encoded = HttpService:JSONEncode(Text)
local Buffer = buffer.fromstring(Encoded)
local Length = buffer.len(Buffer)
local Base64Length = Length - Offset - 2
local Base64Buffer = buffer.create(Base64Length)
buffer.copy(Base64Buffer, 0, Buffer, Offset, Base64Length)
return buffer.tostring(Base64Buffer)
end
local TestString = buffer.fromstring(string.rep("h", 1e6))
local Start = os.clock()
for _ = 1, 1e3 do
local _ = Encode(TestString)
end
warn(`Benchmark: ${os.clock() - Start} seconds`) -- around 0.1 seconds
You can just work with the JSONEncode string directly.
local function Encode(Input: buffer): string
return string.sub(HttpService:JSONEncode(Input), 34, -3)
end
local function Decode(Input: string): buffer
return HttpService:JSONDecode("{\"m\":null,\"t\":\"buffer\",\"base64\":\"" .. Input .. "\"}")
end
Granted, a faster implementation is probably feasible with native calls to buffer.readbits/writebits, but this is definitely the simplest one.
You’re right. I ran a benchmark using a 1000-byte string over 1000 iterations:
Base64.encode: 0.0973 s
JSONEncode(buffer): 0.0047 s
JSONEncode(buffer) + slice: 0.0046 s
The native method is a lot faster, since it’s handled in C++. That said, my goal with this module, was something:
that doesn’t rely on the buffer API,
and is fully transparent, and easy to modify.
It’s mainly for cases where you need reliable Base64 encoding for strings or byte arrays.
Might add an optional buffer-based path later for those who want the speed boost.
Thanks.
Well yes if the string is too long then its a dev problem, it won’t even get past the luau error of long strings with the 1gb limit. You can always process it in chunks
I wrote that in devfourm as an example, you’d be better off doing something like
local HttpService = game:GetService("HttpService")
local ToString = buffer.tostring
local FromString = buffer.fromstring
local function Compress(Text)
return HttpService:JSONEncode(FromString(Text))
end
local function Decompress(Text)
return ToString(HttpService:JSONDecode(Text))
end
return {
Compress = Compress,
Decompress = Decompress
}
Unless you want to create a way to process a string in chunks less than a 100, theres no real way to change it back into normal base64 as I believe the internal compression is custom
local HttpService = game:GetService("HttpService")
local Offset = 34
local function Encode(Text : string) : string
local Encoded = HttpService:JSONEncode(buffer.fromstring(Text))
local Buffer = buffer.fromstring(Encoded)
local Length = buffer.len(Buffer)
local Base64Length = Length - Offset - 2
local Base64Buffer = buffer.create(Base64Length)
buffer.copy(Base64Buffer, 0, Buffer, Offset, Base64Length)
return buffer.tostring(Base64Buffer)
end
local function Decode(Text : string) : string
return buffer.tostring(HttpService:JSONDecode(`\{"m":null,"t":"buffer","{Text:match("^KLUv/") and "zbase64" or "base64"}":"{Text}"\}`))
end
0.1 seconds per gigabyte sounds too good to be true On random data, that would be 6+ seconds.
--!optimize 2
--!native
local HttpService = game:GetService("HttpService")
local Offset = 34
local function Encode(Text)
local Encoded = HttpService:JSONEncode(Text)
local Buffer = buffer.fromstring(Encoded)
local Length = buffer.len(Buffer)
local Base64Length = Length - Offset - 2
local Base64Buffer = buffer.create(Base64Length)
buffer.copy(Base64Buffer, 0, Buffer, Offset, Base64Length)
return buffer.tostring(Base64Buffer)
end
local function genstr(n: number, ascii: boolean)
local b = buffer.create(n)
local min = ascii and 32 or 0
local max = ascii and 126 or 255
for i=0, n-1 do
buffer.writeu8(b, i, math.random(min, max))
end
return buffer.tostring(b)
end
task.wait()
-- binary
local TestString = buffer.fromstring(genstr(1e6, false))
local Start = os.clock()
for _ = 1, 1e3 do
local _ = Encode(TestString)
end
warn(`Benchmark: ${os.clock() - Start} seconds`) -- Benchmark: $6.74043079999683 seconds
task.wait()
-- ascii
local TestString = buffer.fromstring(genstr(1e6, true))
local Start = os.clock()
for _ = 1, 1e3 do
local _ = Encode(TestString)
end
warn(`Benchmark: ${os.clock() - Start} seconds`) -- Benchmark: $6.314211600001727 seconds
1000x is exaggeration, only c++ libraries would be doing it in 0.1 seconds but 6ish seconds isn’t bad when the string is also compressed by zstd which in itself isn’t great for random strings.
Although I managed to get it down to 4.4 seconds using my own base64
If goal is use for DataStores, then it’s better to do base96 encoding. 128 isn’t possible due to some symbols appearing during that, but I did 96 a lot of times.