Quicknum - Fastest Big Number Library

Overview

Quicknum is a module designed to quickly store and operate larger numbers than regular doubles, with plenty of functions to help you make incremental/simulator games. This module can handle calculations of numbers up to 2^2^1024 or 1 followed by around 5.3e307 zeros, while still being faster than similar number modules like BigNum and QubitNum.

Uses

If you are making an incremental/simulator game and you use doubles you are limited to 1.79e308 for storing and calculating which can be very limiting. This module can fix that problem allowing you to use numbers up to 2^1.79e308.

Speed

This is what I have found to be the fastest large number library in luau ( tests below ).
*operation is run 1,000,000 times to find approximate time in nanoseconds ( 1 billionth of a second )

Addition | Subtraction | Multiplication | Division | Reciprocal

  • QubitNum: 215.51ns | 216.36ns | 158.82ns | 157.23ns | 416.44ns
  • BigNum: 73.61ns | 84.05ns | 41.13ns | 42.95ns | 38.85ns
  • QuickNum: 37.75ns | 37.96ns | 32.16ns | 32.43ns | 29.74ns

Power | Root | Modulus | Integer Division | Logarithm

  • QubitNum: 258.14ns | 739.93ns | 890.47ns | 389.46ns | 240.07ns
  • BigNum: 64.10ns | 89.49ns | 140.74ns | 128.01ns | 134.13ns
  • QuickNum: 62.15ns | 62.39ns | 55.34ns | 41.11ns | 53.24ns

There are more functions included but it’s incredibly tedious to test every single one

Simplicity

For speed reasons I can't use metatables or metamethods, but I have found a way to ease the process. By using functions like addeq(), subeq(), muleq(), and many more, to make operations simpler.

Example:
local qn = require(game.ReplicatedStorage.QuickNum)
local number1 = qn.fromNumber(5)
qn.addeq(number1, 1) -- takes approx. 19.46ns
print(qn.tostring(number1)) -- returns 6.00

--As opposed to:

local number2 = qn.fromNumber(5)
number2 = qn.add(number2, 1) -- takes approx. 37.75ns
print(qn.tostring(number2)) -- returns 6.00

This not only cleans up your code but it actually makes it faster, due to not needing to create a temporary quicknum to store the answer of add().

How it Works

Quicknum is a buffer of size 16, first 8 bytes are the mantissa and the last 8 bytes are the exponent, such that the value of the number is mantissa * (2 ^ exponent), this improves speed and accuracy.


full function list at the top of module, please report any bugs you find.

Get it here: Quicknum

3 Likes

Nice, will be using this. This needs to get more attention than the other ones out there.

I was transferring my game’s currency system to use Quicknum and I found a few bugs in the library in the process, specifically related to comparison functions (lt, gt, lte, gte, etc.)

I used AI to fix all the issues, but I’d still like to let you know if it missed anything.

Updated functions that had an issue:

--[[
	n1 < n2
]]
function num.lt(n1, n2)
	local m1, e1, m2, e2

	-- Extraction logic for Input 1
	if type(n1) == "buffer" then
		m1 = buffer.readf64(n1, 0)
		e1 = buffer.readf64(n1, 8)
	elseif type(n1) == "number" then
		m1, e1 = math.frexp(n1)
	else
		error("Wrong Type: lt(), Input 1")
	end

	-- Extraction logic for Input 2
	if type(n2) == "buffer" then
		m2 = buffer.readf64(n2, 0)
		e2 = buffer.readf64(n2, 8)
	elseif type(n2) == "number" then
		m2, e2 = math.frexp(n2)
	else
		error("Wrong Type: lt(), Input 2")
	end

	local sign1 = math.sign(m1)
	local sign2 = math.sign(m2)

	-- 1. Compare Signs
	if sign1 ~= sign2 then
		return sign1 < sign2
	end

	-- 2. Handle Zero
	if sign1 == 0 then return false end

	-- 3. Compare Magnitudes based on Sign
	if sign1 > 0 then
		-- Both Positive: Lower exponent or lower mantissa is smaller
		if e1 ~= e2 then
			return e1 < e2
		else
			return m1 < m2
		end
	else
		-- Both Negative: Higher exponent or lower mantissa is smaller
		if e1 ~= e2 then
			return e1 > e2
		else
			return m1 < m2
		end
	end
end
--[[
	n1 <= n2
]]
function num.lte(n1, n2)
	local m1, e1, m2, e2

	-- Extraction logic for Input 1
	if type(n1) == "buffer" then
		m1 = buffer.readf64(n1, 0)
		e1 = buffer.readf64(n1, 8)
	elseif type(n1) == "number" then
		m1, e1 = math.frexp(n1)
	else
		error("Wrong Type: lte(), Input 1")
	end

	-- Extraction logic for Input 2
	if type(n2) == "buffer" then
		m2 = buffer.readf64(n2, 0)
		e2 = buffer.readf64(n2, 8)
	elseif type(n2) == "number" then
		m2, e2 = math.frexp(n2)
	else
		error("Wrong Type: lte(), Input 2")
	end

	local sign1 = math.sign(m1)
	local sign2 = math.sign(m2) -- Fixed typo: changed m1 to m2

	-- 1. Compare Signs
	if sign1 ~= sign2 then
		return sign1 < sign2
	end

	-- 2. Handle Zero (Both are 0)
	if sign1 == 0 then return true end

	-- 3. Compare Magnitudes based on Sign
	if sign1 > 0 then
		-- Both Positive
		if e1 ~= e2 then
			return e1 < e2
		else
			return m1 <= m2 -- Fixed: compare extracted mantissas, not buffers
		end
	else
		-- Both Negative
		if e1 ~= e2 then
			-- A larger negative exponent means a smaller number
			return e1 > e2
		else
			return m1 <= m2 -- Both negative, so smaller (more negative) mantissa is smaller
		end
	end
end
--[[
	n1 > n2
]]
function num.gt(n1, n2)
	local m1, e1, m2, e2

	-- Extraction logic for Input 1
	if type(n1) == "buffer" then
		m1 = buffer.readf64(n1, 0)
		e1 = buffer.readf64(n1, 8)
	elseif type(n1) == "number" then
		m1, e1 = math.frexp(n1)
	else
		error("Wrong Type: gt(), Input 1")
	end

	-- Extraction logic for Input 2
	if type(n2) == "buffer" then
		m2 = buffer.readf64(n2, 0)
		e2 = buffer.readf64(n2, 8)
	elseif type(n2) == "number" then
		m2, e2 = math.frexp(n2)
	else
		error("Wrong Type: gt(), Input 2")
	end

	local sign1 = math.sign(m1)
	local sign2 = math.sign(m2) -- Fixed typo: changed m1 to m2

	-- 1. Compare Signs
	if sign1 ~= sign2 then
		return sign1 > sign2
	end

	-- 2. Handle Zero (Both are 0)
	if sign1 == 0 then return false end

	-- 3. Compare Magnitudes based on Sign
	if sign1 > 0 then
		-- Both Positive: Larger exponent or larger mantissa is greater
		if e1 ~= e2 then
			return e1 > e2
		else
			return m1 > m2
		end
	else
		-- Both Negative: Smaller exponent or larger mantissa is greater
		-- (Example: -10^2 is greater than -10^5)
		if e1 ~= e2 then
			return e1 < e2
		else
			return m1 > m2
		end
	end
end
--[[
	n1 >= n2
]]
function num.gte(n1, n2)
	local m1, e1, m2, e2

	-- Extraction logic for Input 1
	if type(n1) == "buffer" then
		m1 = buffer.readf64(n1, 0)
		e1 = buffer.readf64(n1, 8)
	elseif type(n1) == "number" then
		m1, e1 = math.frexp(n1)
	else
		error("Wrong Type: gte(), Input 1")
	end

	-- Extraction logic for Input 2
	if type(n2) == "buffer" then
		m2 = buffer.readf64(n2, 0)
		e2 = buffer.readf64(n2, 8)
	elseif type(n2) == "number" then
		m2, e2 = math.frexp(n2)
	else
		error("Wrong Type: gte(), Input 2")
	end

	local sign1 = math.sign(m1)
	local sign2 = math.sign(m2) -- Fixed typo: changed m1 to m2

	-- 1. Compare Signs
	if sign1 ~= sign2 then
		return sign1 > sign2
	end

	-- 2. Handle Zero (Both are 0)
	if sign1 == 0 then return true end

	-- 3. Compare Magnitudes based on Sign
	if sign1 > 0 then
		-- Both Positive: Greater exponent or greater mantissa is gte
		if e1 ~= e2 then
			return e1 > e2
		else
			return m1 >= m2
		end
	else
		-- Both Negative: Smaller exponent or greater (closer to 0) mantissa is gte
		-- (Example: -1*2^2 is greater than -1*2^5)
		if e1 ~= e2 then
			return e1 < e2
		else
			return m1 >= m2
		end
	end
end
--[[
	n1 == n2
]]
function num.eq(n1, n2)
	local m1, e1, m2, e2

	-- Extraction logic for Input 1
	if type(n1) == "buffer" then
		m1 = buffer.readf64(n1, 0)
		e1 = buffer.readf64(n1, 8)
	elseif type(n1) == "number" then
		m1, e1 = math.frexp(n1)
	else
		error("Wrong Type: eq(), Input 1")
	end

	-- Extraction logic for Input 2
	if type(n2) == "buffer" then
		m2 = buffer.readf64(n2, 0)
		e2 = buffer.readf64(n2, 8)
	elseif type(n2) == "number" then
		m2, e2 = math.frexp(n2)
	else
		error("Wrong Type: eq(), Input 2")
	end

	-- 1. Handle Zero (Both are 0)
	if m1 == 0 and m2 == 0 then 
		return true 
	end

	-- 2. Compare using an Epsilon
	-- This ensures that tiny rounding errors don't break your "Buy" buttons.
	-- We check if exponents match and mantissas are "close enough."
	return e1 == e2 and math.abs(m1 - m2) < 1e-14
end

In addition, AI also implemented hexencode and hexdecode functions.

--[[
	Encodes n into hexadecimal for ultra-fast syncing and saving.
	A 16-byte buffer always results in a 32-character string.
]]
function num.hexencode(n)
	if type(n) ~= "buffer" then error("Wrong Type: hexencode(), Input 1") end

	local m = buffer.readf64(n, 0)
	local e = buffer.readf64(n, 8)

	-- Optimized special cases
	if m == 0 then return "0" end
	if m == 0.5 and e == 1 then return "1" end

	local len = buffer.len(n)
	local t = table.create(len)
	for i = 0, len - 1 do
		local b = buffer.readu8(n, i)
		local h = math.floor(b / 16)
		local l = b % 16
		t[i + 1] = string.sub(hex_chars, h + 1, h + 1) .. string.sub(hex_chars, l + 1, l + 1)
	end
	return table.concat(t)
end

--[[
	Decodes from .hexencode() back into a 16-byte buffer.
]]
function num.hexdecode(s)
	if type(s) ~= "string" then error("Wrong Type: hexdecode(), Input 1") end

	if s == "0" then
		return buffer.create(16)
	elseif s == "1" then
		local buf = buffer.create(16)
		buffer.writef64(buf, 0, 0.5)
		buffer.writef64(buf, 8, 1) -- Corrected exponent offset
		return buf
	end

	local buf = buffer.create(16)
	for i = 1, #s, 2 do
		local byte_hex = string.sub(s, i, i + 1)
		local byte_val = hex_to_dec[byte_hex]
		if byte_val then
			buffer.writeu8(buf, (i - 1) / 2, byte_val)
		end
	end
	return buf
end