Manual Memory Management (MMM) for Luau – Skip the GC, Boost Performance!

Hey Roblox devs :waving_hand:

I’m excited to share a new module I’ve built called ManualMemoryManagement (MMM), designed to bring manual memory allocation to Luau in Roblox. This module bypasses the garbage collector (GC), giving you direct control over memory and unlocking performance gains in your scripts.

:wrench: What It Does

  • Allocates memory manually using a 1GB buffer (customizable)
  • Supports multiple data types: u8, i8, u16, i16, u32, i32, f32, f64, and string
  • Includes defragmentation logic to keep memory compact
  • Works best with the --!native flag for AOT compilation
  • Ideal for performance-critical systems like custom physics, data serialization, or memory pools

:warning: Use Responsibly

If used in a LocalScript, memory is allocated to the client’s RAM. Misuse can cause memory leaks, OOM errors, or even BSODs. This is a low-level tool—treat it with care.

:package: Download & Try It

You can grab the module from ModsFire [I have uploaded it here so per each download I can gain a very small margin. It really helps support my development, and you don’t have to donate anything. Make sure to be safe by not clicking on any redirects].
Feel free to explore, test, and integrate it into your projects.

:speech_balloon: Feedback Wanted!

I’d love to hear your thoughts:

  • Would you use manual memory management in Roblox?
  • Any features you’d like added?
  • Suggestions for optimization or safety?

Thanks for checking it out!
— Alexandru2201913

1 Like

Can you provide a preview or something? I’m not trying to download anything from a 3RD party.

it is advised that you upload on the creator hub, because of credibility issues, potential malvertising campaigns from the ModsFire link, and you may get warned for offsite links like these lol

1 Like

Here you go

--[[

ManualMemoryManagement
Made by Alexandru2201913

This module brings Manual Memory Management to Luau in Roblox.
The main reason to use this module is to skip the GC (garbage collector),
effectively improving performance in all scripts.

The default size for allocation in memory is exactly 1GB. You can change
it to whatever size you want, with the condition of staying under 1GB.

If used by a LocalScript, this memory is allocated to the client's RAM.
Use responsibly, as you can cause memory leaks on the client's machine,
which will trigger OOM, which may result in a BSOD and possible data
loss.

This script can be used much more effectively by adding the '--!native'
flag to your scripts. They will be compiled AOT, and memory operations
are much faster done by a compiled environment than an interpreted one.

]]

--!native

-- Master Instances
local ManualMemoryManagement = {}					-- Module that will store all of our functions.
local buffer_instance = buffer.create(1_073_741_824)-- 1GB buffer, you can set this to any value you want.

-- Offsets
local id_offset = 0									-- Global ID variable, it always rises.
local current_offset = 0							-- Current offset in memory.

-- Metatables
local uid_to_oid_vsize = {}							-- A table that contains the metadata of all allocated objects.

-- Flags
local defragging = false							-- Flag to make sure allocation doesn't happen during defragmentation.

-- Functions

-- Allocate an u8 to memory
-- This object can have the values 0<>255
function ManualMemoryManagement.alloc_u8(value: number): number
	
	repeat task.wait() until defragging == false
	
	if current_offset + 1 > 1_073_741_824 then
		return -1
	end
	
	local start_offset = current_offset
	buffer.writeu8(buffer_instance, start_offset, value)
	current_offset += 1
	
	uid_to_oid_vsize[id_offset] = {start_offset, 1, false, "u8"}
	id_offset += 1
	
	return id_offset - 1
end

-- Allocate an i8 to memory
-- This object can have the values -128<>127
function ManualMemoryManagement.alloc_i8(value: number): number
	
	repeat task.wait() until defragging == false
	
	if current_offset + 1 > 1_073_741_824 then
		return -1
	end
	
	local start_offset = current_offset
	buffer.writei8(buffer_instance, start_offset, value)
	current_offset += 1

	uid_to_oid_vsize[id_offset] = {start_offset, 1, false, "i8"}
	id_offset += 1

	return id_offset - 1
end

-- Allocate an u16 to memory
-- This object can have the values 0<>65,535
function ManualMemoryManagement.alloc_u16(value: number): number
	
	repeat task.wait() until defragging == false
	
	if current_offset + 2 > 1_073_741_824 then
		return -1
	end
	
	local start_offset = current_offset
	buffer.writeu16(buffer_instance, start_offset, value)
	current_offset += 2

	uid_to_oid_vsize[id_offset] = {start_offset, 2, false, "u16"}
	id_offset += 1

	return id_offset - 1
end

-- Allocate an i16 to memory
-- This object can have the values -32,768<>32,767
function ManualMemoryManagement.alloc_i16(value: number): number
	
	repeat task.wait() until defragging == false
	
	if current_offset + 2 > 1_073_741_824 then
		return -1
	end
	
	local start_offset = current_offset
	buffer.writei16(buffer_instance, start_offset, value)
	current_offset += 2

	uid_to_oid_vsize[id_offset] = {start_offset, 2, false, "i16"}
	id_offset += 1

	return id_offset - 1
end

-- Allocate an u32 to memory
-- This object can have the values 0<>4,294,967,295
function ManualMemoryManagement.alloc_u32(value: number): number
	
	repeat task.wait() until defragging == false
	
	if current_offset + 4 > 1_073_741_824 then
		return -1
	end
	
	local start_offset = current_offset
	buffer.writeu32(buffer_instance, start_offset, value)
	current_offset += 4

	uid_to_oid_vsize[id_offset] = {start_offset, 4, false, "u32"}
	id_offset += 1

	return id_offset - 1
end

-- Allocate an i32 to memory
-- This object can have the values -2,147,483,648<>2,147,483,647
function ManualMemoryManagement.alloc_i32(value: number): number

	repeat task.wait() until defragging == false
	
	if current_offset + 4 > 1_073_741_824 then
		return -1
	end
	
	local start_offset = current_offset
	buffer.writei32(buffer_instance, start_offset, value)
	current_offset += 4

	uid_to_oid_vsize[id_offset] = {start_offset, 4, false, "i32"}
	id_offset += 1

	return id_offset - 1
end

-- Allocate an f32 to memory
-- This object is a single-precision floating-point number
function ManualMemoryManagement.alloc_f32(value: number): number
	repeat task.wait() until defragging == false

	if current_offset + 4 > 1_073_741_824 then
		return -1
	end

	local start_offset = current_offset
	buffer.writef32(buffer_instance, start_offset, value)
	current_offset += 4

	uid_to_oid_vsize[id_offset] = {start_offset, 4, false, "f32"}
	id_offset += 1

	return id_offset - 1
end
-- Allocate an f64 to memory
-- This object is a double-precision floating-point number
function ManualMemoryManagement.alloc_f64(value: number): number
	
	repeat task.wait() until defragging == false
	
	if current_offset + 8 > 1_073_741_824 then
		return -1
	end
	
	local start_offset = current_offset
	buffer.writef64(buffer_instance, start_offset, value)
	current_offset += 8

	uid_to_oid_vsize[id_offset] = {start_offset, 8, false, "f64"}
	id_offset += 1

	return id_offset - 1
end

-- Allocate a string to memory
-- This object can have any value, but it must be a string
function ManualMemoryManagement.alloc_str(value: string): number
	
	repeat task.wait() until defragging == false
	
	if current_offset + #value > 1_073_741_824 then
		return -1
	end
	
	local start_offset = current_offset
	buffer.writestring(buffer_instance, start_offset, value)
	current_offset += #value

	uid_to_oid_vsize[id_offset] = {start_offset, #value, true}
	id_offset += 1

	return id_offset - 1
end

-- Read a pointer from memory
-- This pointer can be any type of object, it will be returned as a number, string or nil
function ManualMemoryManagement.read(ptr): string | number | nil
	
	repeat task.wait() until defragging == false
	
	if uid_to_oid_vsize[ptr] then
		if uid_to_oid_vsize[ptr][3] == true then
			return buffer.readstring(buffer_instance, uid_to_oid_vsize[ptr][1], uid_to_oid_vsize[ptr][2])
		else
			if uid_to_oid_vsize[ptr][4] == "u8" then
				return buffer.readu8(buffer_instance, uid_to_oid_vsize[ptr][1])
			elseif uid_to_oid_vsize[ptr][4] == "i8" then
				return buffer.readi8(buffer_instance, uid_to_oid_vsize[ptr][1])
			elseif uid_to_oid_vsize[ptr][4] == "u16" then
				return buffer.readu16(buffer_instance, uid_to_oid_vsize[ptr][1])
			elseif uid_to_oid_vsize[ptr][4] == "i16" then
				return buffer.readi16(buffer_instance, uid_to_oid_vsize[ptr][1])
			elseif uid_to_oid_vsize[ptr][4] == "u32" then
				return buffer.readu32(buffer_instance, uid_to_oid_vsize[ptr][1])
			elseif uid_to_oid_vsize[ptr][4] == "i32" then
				return buffer.readi32(buffer_instance, uid_to_oid_vsize[ptr][1])
			elseif uid_to_oid_vsize[ptr][4] == "f32" then
				return buffer.readf32(buffer_instance, uid_to_oid_vsize[ptr][1])
			elseif uid_to_oid_vsize[ptr][4] == "f64" then
				return buffer.readf64(buffer_instance, uid_to_oid_vsize[ptr][1])
			end
		end
	else
		return nil
	end
end

-- Free an object from memory
-- This function will defrag the memory pool, eliminate the pointer from metatable and free the memory itself
function ManualMemoryManagement.free(uid)
	
	if uid_to_oid_vsize[uid] then
		
		defragging = true
		
		task.spawn(function()
			for k, v in pairs(uid_to_oid_vsize) do
				if v[1] <= uid_to_oid_vsize[uid][1] then task.wait() continue end
				
				if uid_to_oid_vsize[uid][3] == false then
					if v[4] == "u8" then
						buffer.writeu8(buffer_instance, v[1] - uid_to_oid_vsize[uid][2], buffer.readu8(buffer_instance, v[1]))
						v[1] -= uid_to_oid_vsize[uid][2]
					elseif v[4] == "i8" then
						buffer.writei8(buffer_instance, v[1] - uid_to_oid_vsize[uid][2], buffer.readi8(buffer_instance, v[1]))
						v[1] -= uid_to_oid_vsize[uid][2]
					elseif v[4] == "u16" then
						buffer.writeu16(buffer_instance, v[1] - uid_to_oid_vsize[uid][2], buffer.readu16(buffer_instance, v[1]))
						v[1] -= uid_to_oid_vsize[uid][2]
					elseif v[4] == "i16" then
						buffer.writei16(buffer_instance, v[1] - uid_to_oid_vsize[uid][2], buffer.readi16(buffer_instance, v[1]))
						v[1] -= uid_to_oid_vsize[uid][2]
					elseif v[4] == "u32" then
						buffer.writeu32(buffer_instance, v[1] - uid_to_oid_vsize[uid][2], buffer.readu32(buffer_instance, v[1]))
						v[1] -= uid_to_oid_vsize[uid][2]
					elseif v[4] == "i32" then
						buffer.writei32(buffer_instance, v[1] - uid_to_oid_vsize[uid][2], buffer.readi32(buffer_instance, v[1]))
						v[1] -= uid_to_oid_vsize[uid][2]
					elseif v[4] == "f32" then
						buffer.writef32(buffer_instance, v[1] - uid_to_oid_vsize[uid][2], buffer.readf32(buffer_instance, v[1]))
						v[1] -= uid_to_oid_vsize[uid][2]
					elseif v[4] == "f64" then
						buffer.writef64(buffer_instance, v[1] - uid_to_oid_vsize[uid][2], buffer.readf64(buffer_instance, v[1]))
						v[1] -= uid_to_oid_vsize[uid][2]
					end
				else
					buffer.writestring(buffer_instance, v[1] - uid_to_oid_vsize[uid][2], buffer.readstring(buffer_instance, v[1], v[2]))
					v[1] -= uid_to_oid_vsize[uid][2]
				end
				task.wait()
			end
			
			current_offset -= uid_to_oid_vsize[uid][2]
			uid_to_oid_vsize[uid] = nil
			defragging = false
		end)
		
	else
		return -1
	end
end

function ManualMemoryManagement.getptrmeta(ptr: number)
	return uid_to_oid_vsize[ptr]
end

return ManualMemoryManagement

Some agree on this.
But I think it is inefficient to allocate enough buffers in advance.
I don’t think the module is very welcome from my point of view of using buffer as useful.

1 Like

You said “to allocate enough buffers” — as you can see I use only one buffer as a memory pool, writing values to it is possible with my functions, reading is possible, and it has defragging functionality too
It is basically a heap allocator and I used C style management, so it should be very efficient, more efficient than the normal variables that are Garbage Collected which introduces hiccups

This is not a performance improvement.
If you check the internal operation method, it goes through string comparison with if statement, which is very inefficient.
There is also a contradiction in what you say, and every time you assign it, you write it as a table in the “uid_to_oid_vsize” table.
In other words, using this module will be less performing than before.

1 Like

It’s a very good attitude to be interested in the content of performance improvement.
I also mainly use the c++, luau language.
In luau, it is better to store the numbers in pure number type rather than in buffer for efficient numerical comparison and operations.

Thanks for the response!
I also code a lot in c++, and I really like the way it handles memory. I feel like it is much faster than GC languages such as C#, which is garbage collected on the .NET framework.

This may see a performance decrease in the sense of light tasks, but the advantage of no GC, which adds a ton of boilerplate, shines in performance intensive tasks.

I will test tho and be back with some results on some light and advanced tasks, if I see some bad results I will let you know and I will say in the title this sees a performance decrease.

1 Like