LUA vs JS benchmark - Free and Standalone

What it does


This resource adds commands to run similar functions in both JavaScript and LUA. It does so in bulk, meaning you should be able to see which language is objectively “faster”.

Currently it’s only doing string concatenation in various ways as well as coordinate based distance calculation, since I believe this makes up a vast majority of scripts. I doubt that regular natives will see any performance boost from using one language over the other though, so I didn’t implement anything there.


Commands


[benchmark] [type]

  • [benchmark]: Either benchmark_js or benchmark_lua.
  • [type]:
    • Concat1: String concatenation with the .. operator in LUA and + operator in JS. Commonly used in scripts.
    • Concat2: String concatenation by using string.format in LUA and ${} in JS. Less common.
    • Concat3: String concatenation via pushing to tables/objects, then joining them. Rarely used.
    • Dist1: Calculating the distance between two random coords, excluding height.
    • Dist2: Calculating the distance between two random coords, including height.


Disclaimer


  • If the results are incorrect, which is likely, please don’t hesitate to correct me.
  • The functions are being run with very high iterators in order to see any difference at all.
  • The functions I use to time everything might not work exactly the same, so the actual execution time could be off by a bit.
  • More functions may be added later.
  • Some parts of the code are definitely not 100% optimized, especially since the concat3 benchmark should technically be the fastest out of them all in JavaScript, but I believe that that’s down to how I’m working with the variables and generating the string. Perhaps pre-generated strings could work faster.
  • Timing on the client side for LUA is done via server triggers, but it shouldn’t increase the results much since it’s triggering both at the start of a timer and at the end.
  • I don’t know C# well enough to test it too ;-; (Edit: Taking C#, C++ and general application development classes soon so I might update this resource to incorporate that at some point)


Download


GitHub


Data


Server JavaScript LUA
Concatenation 90ms 1644ms
2D Distance 132ms 3926ms
3D Distance 153ms 1878ms
Client JavaScript LUA
Concatenation 90ms 1373ms
2D Distance 137ms 2234ms
3D Distance 113ms 861ms

*Using fastest averages only over 20 runs, basically how the script is right now in the repo, restarting the server between each run. Running on a 5800X, locally hosted, 32 GB 3200 MHz RAM, server version 6683, stable client.

If the results are correct then that means that LUA is on average 7 to 30 times slower than JavaScript. So would I recommend rewriting everything in JS and ditching LUA? Only if you know JavaScript just as good as LUA and you need every bit of performance you can get. But overall, it won’t really be worth the hassle.

So why did I make this resource? Boredom.

5 Likes

Nicely done! :smile:

This doesn’t really take into consideration other factors (mainly native execution).

Also if this text wanted to be completely correct it should include C# which would likely stomp out both of them.

2 Likes

You’re right and I thought I mentioned that somewhere but apparantly I didn’t. For a real-world test I will be attempting to convert a resource from LUA to JavaScript, don’t know which one just yet.

As for C# though, at least I did mention that. Since I’ve had very few run-ins with it, I can’t confidently create a C# resource that will show what it can actually do.

Edit: Picked SimpleCarHUD because it’s easy to work with and includes many regular natives, plus there should be plenty of room for optimizing. For now I’ve only converted it to JS without any optimization because it’s getting late, file here:
cl.js (11.9 KB)

I’m done converting and optimizing the JavaScript version of SimpleCarHUD, although it’s not 100% optimized and I didn’t test super extensively.

Code here
Delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));



// Configuration
const screen_pos_x = 0.165;
const screen_pos_y = 0.882;
const always_show = false;								// Show the HUD when on foot?

const seatbelt_plays_sound = true;						// Play a sound when toggling the seatbelt?
const seatbelt_eject_min_speed = 5;						// In meters per second.
const force_to_eject = 400;								// Deceleration in meters per second.

const seatbelt_keyboard = "K";
const seatbelt_controller = "LDOWN_INDEX";
const cruise_keyboard = "CAPITAL";
const cruise_controller = "RDOWN_INDEX";

const speed_limit = 100.0;
const low_fuel = 25.0;

const text_color_good = [96, 255, 96, 255];
const text_color_neutral = [255, 255, 255, 255];
const text_color_bad = [255, 96, 96, 255];

const classes = {
	speed: [13],										// Hide speed display for these classes
	fuel: [13, 21],										// Hide fuel display for these classes
	cruise: [13, 14, 15, 16, 21, 22],					// Hide cruise display for these classes
	seatbelt: [8, 13]									// Hide seatbelt display for these classes
};

const headings = ["N", "NW", "W", "SW", "S", "SE", "E", "NE", "N"];
const zones = {
	AIRP: "Los Santos International Airport",
	ALAMO: "Alamo Sea",
	ALTA: "Alta",
	ARMYB: "Fort Zancudo",
	BANHAMC: "Banham Canyon Dr",
	BANNING: "Banning",
	BEACH: "Vespucci Beach",
	BHAMCA: "Banham Canyon",
	BRADP: "Braddock Pass",
	BRADT: "Braddock Tunnel",
	BURTON: "Burton",
	CALAFB: "Calafia Bridge",
	CANNY: "Raton Canyon",
	CCREAK: "Cassidy Creek",
	CHAMH: "Chamberlain Hills",
	CHIL: "Vinewood Hills",
	CHU: "Chumash",
	CMSW: "Chiliad Mountain State Wilderness",
	CYPRE: "Cypress Flats",
	DAVIS: "Davis",
	DELBE: "Del Perro Beach",
	DELPE: "Del Perro",
	DELSOL: "La Puerta",
	DESRT: "Grand Senora Desert",
	DOWNT: "Downtown",
	DTVINE: "Downtown Vinewood",
	EAST_V: "East Vinewood",
	EBURO: "El Burro Heights",
	ELGORL: "El Gordo Lighthouse",
	ELYSIAN: "Elysian Island",
	GALFISH: "Galilee",
	GOLF: "GWC and Golfing Society",
	GRAPES: "Grapeseed",
	GREATC: "Great Chaparral",
	HARMO: "Harmony",
	HAWICK: "Hawick",
	HORS: "Vinewood Racetrack",
	HUMLAB: "Humane Labs and Research",
	JAIL: "Bolingbroke Penitentiary",
	KOREAT: "Little Seoul",
	LACT: "Land Act Reservoir",
	LAGO: "Lago Zancudo",
	LDAM: "Land Act Dam",
	LEGSQU: "Legion Square",
	LMESA: "La Mesa",
	LOSPUER: "La Puerta",
	MIRR: "Mirror Park",
	MORN: "Morningwood",
	MOVIE: "Richards Majestic",
	MTCHIL: "Mount Chiliad",
	MTGORDO: "Mount Gordo",
	MTJOSE: "Mount Josiah",
	MURRI: "Murrieta Heights",
	NCHU: "North Chumash",
	NOOSE: "N.O.O.S.E",
	OCEANA: "Pacific Ocean",
	PALCOV: "Paleto Cove",
	PALETO: "Paleto Bay",
	PALFOR: "Paleto Forest",
	PALHIGH: "Palomino Highlands",
	PALMPOW: "Palmer-Taylor Power Station",
	PBLUFF: "Pacific Bluffs",
	PBOX: "Pillbox Hill",
	PROCOB: "Procopio Beach",
	RANCHO: "Rancho",
	RGLEN: "Richman Glen",
	RICHM: "Richman",
	ROCKF: "Rockford Hills",
	RTRAK: "Redwood Lights Track",
	SANAND: "San Andreas",
	SANCHIA: "San Chianski Mountain Range",
	SANDY: "Sandy Shores",
	SKID: "Mission Row",
	SLAB: "Stab City",
	STAD: "Maze Bank Arena",
	STRAW: "Strawberry",
	TATAMO: "Tataviam Mountains",
	TERMINA: "Terminal",
	TEXTI: "Textile City",
	TONGVAH: "Tongva Hills",
	TONGVAV: "Tongva Valley",
	VCANA: "Vespucci Canals",
	VESP: "Vespucci",
	VINE: "Vinewood",
	WINDF: "Ron Alternates Wind Farm",
	WVINE: "West Vinewood",
	ZANCUDO: "Zancudo River",
	ZP_ORT: "Port of South Los Santos",
	ZQ_UAR: "Davis Quartz",
	PROL: "Prologue / North Yankton",
	ISHeist: "Cayo Perico Island"
};



// Variables
var player_ped = 0;
var player_pos_x = 0;
var player_pos_y = 0;
var player_pos_z = 0;
var player_prev_velocity_x = 0;
var player_prev_velocity_y = 0;
var player_prev_velocity_z = 0;
var player_pos_h = 0;

var player_vehicle = 0;
var player_vehicle_class = -1;
var player_vehicle_speed = 0;
var player_vehicle_fuel = "000";
var player_vehicle_speed_old = 0;

var use_metric = false;
var speed_multiplier = 0;
var fuel_multiplier = 0;

var speed_color = text_color_neutral;
var fuel_color = text_color_neutral;
var cruise_color = text_color_neutral;
var seatbelt_color = text_color_bad;
var vehicle_has_speed = false;
var vehicle_has_fuel = false;
var vehicle_has_cruise = false;
var vehicle_has_seatbelt = false;

var cruise_status = false;
var seatbelt_status = false;

var speed_fuel_string = "";
var time_string = "";
var location_string = "";

var seatbelt_handler;
var can_be_ejected = false;



// Low priority updates | 1s
setTick(async () => {

	// Basic info
	player_ped = PlayerPedId();
	player_vehicle = GetVehiclePedIsIn(player_ped, false);

	// Update only if the HUD is visible
	if (always_show || player_vehicle !== 0) {
		[player_pos_x, player_pos_y, player_pos_z] = GetEntityCoords(player_ped, false);
		player_pos_h = GetEntityHeading(player_ped);
		use_metric = ShouldUseMetricMeasurements();

		// Only update if in vehicle
		if (player_vehicle !== 0) {
			player_vehicle_class = GetVehicleClass(player_vehicle);
			can_be_ejected = GetEntitySpeedVector(player_vehicle, true)[1] > seatbelt_eject_min_speed;

			// Update cruise control and seatbelt availability
			vehicle_has_fuel = !classes.fuel.includes(player_vehicle_class);
			vehicle_has_speed = !classes.speed.includes(player_vehicle_class);
			vehicle_has_cruise = !classes.cruise.includes(player_vehicle_class);
			vehicle_has_seatbelt = !classes.seatbelt.includes(player_vehicle_class);

			// Has speedo and/or fuel
			if (vehicle_has_speed && vehicle_has_fuel) {
				speed_fuel_string = ((use_metric) ? "KPH                    " : "MPH                    ") + ((use_metric) ? "L" : "GAL");
				speed_multiplier = (use_metric) ? 3.6 : 2.23694;
				fuel_multiplier = (use_metric) ? 1 : 0.264172;
				player_vehicle_fuel = String(Math.floor(GetVehicleFuelLevel(player_vehicle) * fuel_multiplier)).padStart(3, "0");
			} else if (vehicle_has_speed && !vehicle_has_fuel) {
				speed_fuel_string = (use_metric) ? "KPH" : "MPH";
				speed_multiplier = (use_metric) ? 3.6 : 2.23694;
			} else if (!vehicle_has_speed && vehicle_has_fuel) {
				speed_fuel_string = (use_metric) ? "                           L" : "                           GAL";
				fuel_multiplier = (use_metric) ? 1 : 0.264172;
				player_vehicle_fuel = String(Math.floor(GetVehicleFuelLevel(player_vehicle) * fuel_multiplier)).padStart(3, "0");
			}
		}

		// Update strings to display
		location_string = headings[Math.floor((player_pos_h + 22.5) / 45)] + " | " + GetStreetNameFromHashKey(GetStreetNameAtCoord(player_pos_x, player_pos_y, player_pos_z)[0]) + " | " + zones[GetNameOfZone(player_pos_x, player_pos_y, player_pos_z)];
		var hour = GetClockHours();
		if (use_metric === true) {
			time_string = String(hour).padStart(2, "0") + ":" + String(GetClockMinutes()).padStart(2, "0");
		} else {
			time_string = String(hour % 12).padStart(2, "0") + ":" + String(GetClockMinutes()).padStart(2, "0") + ((hour < 13) ? " AM" : " PM");
		}
	}

	await Delay(1000);
});

// High priority updates | Every tick
setTick(async () => {

	// If in a vehicle
	if (player_vehicle !== 0) {

		// Update speed
		player_vehicle_speed_old = player_vehicle_speed;
		player_vehicle_speed = GetEntitySpeed(player_vehicle);

		// Seatbelt logic
		if (vehicle_has_seatbelt) {
			if (!seatbelt_status) {
				if ((player_vehicle_speed_old - player_vehicle_speed) / GetFrameTime() >= force_to_eject && can_be_ejected) {

					// Force update coords right now
					[player_pos_x, player_pos_y, player_pos_z] = GetEntityCoords(player_ped, false);

					// Set player coords outside of vehicle
					SetEntityCoords(player_ped, player_pos_x, player_pos_y, player_pos_z + 0.4, true, true, true, false);
					SetEntityVelocity(player_ped, player_prev_velocity_x, player_prev_velocity_y, player_prev_velocity_z);
					await Delay(0);
					SetPedToRagdoll(player_ped, 2000, 2000, 0, false, false, false);

					// Clear variables
					player_vehicle = 0;
					player_vehicle_class = -1;
					can_be_ejected = false;
				}
				[player_prev_velocity_x, player_prev_velocity_y, player_prev_velocity_z] = GetEntityVelocity(player_ped);
			}
		}
	}
});



// Displaying generated strings | Every tick
setTick(async () => {

	// Draw location and time
	if (player_vehicle !== 0 || always_show) {
		drawTxt(time_string, 4, text_color_neutral, 0.4, screen_pos_x, screen_pos_y + 0.048);
		drawTxt(location_string, 4, text_color_neutral, 0.5, screen_pos_x, screen_pos_y + 0.075);
	}

	// Draw remainder of HUD if in vehicle
	if (player_vehicle !== 0) {
		if (vehicle_has_speed) {
			drawTxt(String(Math.ceil(player_vehicle_speed * speed_multiplier)).padStart(3, "0"), 4, speed_color, 0.8, screen_pos_x, screen_pos_y);
		}

		if (vehicle_has_fuel) {
			drawTxt(player_vehicle_fuel, 4, fuel_color, 0.8, screen_pos_x + 0.055, screen_pos_y);
		}

		if (vehicle_has_speed || vehicle_has_fuel) {
			drawTxt(speed_fuel_string, 4, text_color_neutral, 0.4, screen_pos_x + 0.03, screen_pos_y + 0.018);
		}

		if (vehicle_has_cruise) {
			drawTxt("CRUISE", 2, cruise_color, 0.4, screen_pos_x + 0.04, screen_pos_y + 0.048);
		}

		if (vehicle_has_seatbelt) {
			drawTxt("SEATBELT", 2, seatbelt_color, 0.4, screen_pos_x + 0.08, screen_pos_y + 0.048);
		}
	}
});



// Drawing text on screen
const drawTxt = async function(content, font, [color_r, color_g, color_b], scale, pos_x, pos_y) {
	BeginTextCommandDisplayText("STRING");
	SetTextFont(font);
	SetTextColour(color_r, color_g, color_b, 255);
	SetTextScale(scale, scale);
	SetTextOutline();
	AddTextComponentSubstringPlayerName(content);
	EndTextCommandDisplayText(pos_x, pos_y);
}



// Toggle cruise and seatbelt status
const toggleCruise = function() {
	if (vehicle_has_cruise) {
		cruise_status = !cruise_status;
		if (cruise_status) {

			// Cruise was turned on, set to current speed
			SetVehicleMaxSpeed(player_vehicle, GetEntitySpeed(player_vehicle));
			cruise_color = text_color_good;
		} else {

			// Cruise was turned off, reset
			SetVehicleMaxSpeed(player_vehicle, -1.0);
			cruise_color = text_color_neutral;
		}
	}
}
const toggleSeatbelt = function() {
	if (vehicle_has_seatbelt) {
		seatbelt_status = !seatbelt_status;
		if (seatbelt_plays_sound) PlaySoundFrontend(-1, "Faster_Click", "RESPAWN_ONLINE_SOUNDSET", true);
		if (seatbelt_status) {
			seatbelt_color = text_color_good;
		} else {
			seatbelt_color = text_color_bad;
		}
	}
}



// Keybinds
RegisterCommand("cruise", toggleCruise);
RegisterCommand("seatbelt", toggleSeatbelt);
RegisterKeyMapping("cruise", "Toggle Cruise Control", "KEYBOARD", cruise_keyboard);
RegisterKeyMapping("~!cruise", "Toggle Cruise Control", "PAD_DIGITALBUTTON", cruise_controller);
RegisterKeyMapping("seatbelt", "Toggle Seatbelt", "KEYBOARD", seatbelt_keyboard);
RegisterKeyMapping("~!seatbelt", "Toggle Seatbelt", "PAD_DIGITALBUTTON", seatbelt_controller);

It’s running at 0.14 - 0.16ms compared to the LUA version which runs at 0.12ms according to the resource monitor.

What I’ve noticed is that when you are running a resource with an empty JavaScript file loaded on the client, the resource monitor already reports 0.02ms CPU time, whereas LUA is at 0.00ms. Same thing for the server side, but there it’s at 0.03ms.

Since I only focused on execution time and not resmon usage at first, I completely missed this.

Anyways, if this scales to larger resources too (0.02ms base usage, 15-30% higher overall usage), then I’d choose LUA over JavaScript. The only thing left now is to test C#, so if anyone here happens to know C# and has some free time on their hands…

why is your benchmark using client/server event for lua and date.now() for JS
you can’t compare both if you do that - you cau use GetNetworkTimeAccurate you might get better results

and yeah it could be nice to also test native execution between languages

I figured that since it’s a local setup, the ping won’t fluctuate at all, so using triggers for it on the client side of lua seemed good enough - Since any delay introduced by it will be applied both at the start and the end of the benchmark, thus resulting in a “good enough” from me. Thanks for the tip though, I’ll update the code tomorrow.

why not just run the benchmark on the server?

I’ve done both, client and server, for JavaScript and LUA.

Native execution wont make a significant difference as its a simple ffi call to the fivem c++ layer, which make the native call, the major difference will be in the serialization/ deserialization part.

What is interesting is to compare the runtime modules differences.

Updated the resource. Overall the time it takes did increase, but that could be down to me testing in canary, not having the lowest graphics settings like I did with the initial testing, having more programs open, and a number of other factors.

I need to clean up my code before I post it but my own benchmarking results are fairly different. Haven’t bothered with the distance checks yet.

1 Like

The concatenation test is duplicated for Lua as they would have the same times.

It should be noted that C# could not finish the string concatenation test because of it frequently trying to do stop-the-world garbage collection.

All of these tests were done with 1,000,000 iterations

Client Lua OAL Lua JS C# rt2
Concatenation 16.8s 16.8s 180.97ms -
2D Distance 180.99ms 178.83ms 33.23ms 4.16ms
3D Distance 184.48ms 183.60ms 33.34ms 6.71ms
Native Execution 198.71ms 637.99ms 697.24 86.58ms

A funny thing:

In C# even if you randomly generate the numbers inside the loop its is still 3x faster than Lua.

EDIT:

Updated Lua non-oal, if you pass the vector instead of manually unpacking (i.e vec.x, vec.y, vec.z) it drops the time by 60ms

Source code: GitHub - AvarianKnight/fivem-benchmarks

1 Like

Follow up to my last post with different iteration times

It should be noted that C# could not finish the string concatenation test because of it frequently trying to do stop-the-world garbage collection because every concatenation would create a new string object.

All of these tests were done with 1,000,000 iterations

Client Lua OAL Lua JS C# rt2
Native Execution 198.71ms 637.99ms 697.24 86.58ms
Concatenation 16.8s 16.8s 180.97ms DNF
2D Distance 180.99ms 178.83ms 33.23ms 4.16ms
3D Distance 184.48ms 183.60ms 33.34ms 6.71ms

All of these tests were done with 10,000 iterations

Client Lua OAL Lua JS C# rt2
Native Execution 2.40ms 6.19ms 48.15ms 1.59ms
Concatenation 1.80ms 1.89ms 4.72ms 40.83ms
2D Distance 1.89ms 2.43ms 15.99ms 0.13ms
3D Distance 1.91ms 1.86ms 15.87ms 90µs

All of these tests were done with 1,000,000 iterations with the optimized way to concatenate a large amount of strings in the specific runtime.

For C# this would be using StringBuilder, in Lua this would be adding all the strings to a table and using table.concat to make the string afterwards.

JS does not have an equivelent optimization (hence why its the slowest here)

Client Lua OAL Lua JS C# rt2
Concatenation 61.52ms 61.52ms 180.97ms 4.71ms

All of these tests were done with 10,000 iterations with the optimized way to concatenate a large amount of strings in the specific runtime.

Client Lua OAL Lua JS C# rt2
Concatenation 0.81ms 0.81ms 5.34ms 0.42ms