dwyl / dwyl/learn-zig

Ziggy Nif vs WASI vs WASM- speed comparison

Open
#18 2 comments 2 reactions 0 assignees View on GitHub
Dominant language
No language data
Stars
4
Forks
1
PR merge metrics
No merged PRs in 30d

Description

So I have my new preferred computation machine that I coded in `Zig`. Time to play with it.... 🐇🕳️

I was curious about the difference in execution time between NIF and WebAssembly.

- __NIF__ with `Zigler`
- __WASI__ with `Wasmex`

And the winner is __`NIF`__, about two times faster.

Zig code compiled `ReleaseFast` in all cases.

The main difference between the two solutions is the way you pass data to and from the host

To receive data in Elixir:
- with `Zigler`, you can receive a struct as a map
- with `Wasmex`, you receive a memory index when the data is in binary form

To pass data from Elixir:
- with `Zigler`, you pass an array of data
- with `Wasmex`, you write data to an index that has been allocated by `Zig`. This way, you can pass strings or serialised data.

> `Zigler` compiles the `Zig` for you whilst you need to `zig build` and target `.wasi` for WebAssembly.



build.zig for WASI

```zig
const std = @import("std");
pub fn build(b: *std.Build) void {
// the `.os_tag` is `.freestanding` when used in the browser
const target = b.resolveTargetQuery(.{
.cpu_arch = .wasm32,
.os_tag = .wasi,
});

// const target = b.standardTargetOptions(.{});
// const optimize = b.standardOptimizeOption(.{ .preferred_optimize_mode = .ReleaseSafe });

const exe = b.addExecutable(.{
.name = "ga_browser",
.root_source_file = b.path("ga_wasm.zig"),
.target = target,
.optimize = .ReleaseFast,
});
exe.entry = .disabled;
exe.rdynamic = true;
// exe.import_memory = true;
// exe.export_memory = true;

exe.initial_memory = 512 * std.wasm.page_size; // 512 pages of 64KiB
b.installArtifact(exe);
}
```


The results of the Genetic Algorithm ran 32 times to collect stats back in `Elixir` are:

```elixir
# Threaded NIF
iex(24)> :timer.tc(fn -> GAThreaded.calc(10, "I want a beer! What about you?!") end)

{2_624_277,
%{max: 1733, min: 545, elitism: 10, mean: 969.25, std_dev: 467.44238906711723}}

# unthreaded NIF
iex(26)> :timer.tc(fn -> GAStd.calc(10, "I want a beer! What about you?!") end)

{2_038_405,
%{max: 1926, min: 537, elitism: 10, mean: 994.46875, std_dev: 347.26}

# WASI
iex(29)> :timer.tc(fn -> GAWasi.calc(10, "I want a beer! What about you?!") end)
{4613034,
%{max: 1532, min: 474, elitism: 10, mean: 894.25, std_dev: 264.93, trials: 32}}

```

> the "threaded" version (using OS threads by `Zig` does not bring anything. The overhead is not worth it in this configuration.

> I measured the time needed to read and compile the "wasm" file by `Wasmex` is around 50ms. It is fast but I don't know if it can be "cached" in some way.

What does the Elixir code look like?

```elixir
defmodule GAThreaded do
use Zig,
otp_app: :ex_genetic_zig,
zig_code_path: "ga_threaded.zig",
# leak_check: true,
release_mode: "RealeaseFast"

def calc(elitism_rate, input) do
run(elitism_rate, input)
end
end

defmodule GAStd do
use Zig,
otp_app: :ex_genetic_zig,
zig_code_path: "ga_zigler.zig",
# leak_check: true,
release_mode: "RealeaseFast"

def calc(elitism_rate, input) do
run(elitism_rate, input)
end
end

defmodule GAWasi do
def calc(elitism, input) do
bin = File.read!("./zig/zig-out/bin/ga.wasm")
len = String.length(input)

{:ok, pid} = Wasmex.start_link(%{bytes: bin, wasi: true})

{:ok, store} = Wasmex.store(pid)
{:ok, memory} = Wasmex.memory(pid)

{:ok, [input_idx]} = Wasmex.call_function(pid, "alloc", [len])
:ok = Wasmex.Memory.write_binary(store, memory, input_idx, input)
{:ok, _} = Wasmex.call_function(pid, "set_target", [input_idx, len])

{:ok, [index]} = Wasmex.call_function(pid, "run", [elitism], 20_000)
response = Wasmex.Memory.read_binary(store, memory, index, 8 * 5)
Wasmex.call_function(pid, "free", [index, 8 * 5])

<> = response

%{elitism: elitism, mean: mean, std_dev: std_dev, min: min, max: max}
end
end
```

The Zig code of the WASI machine.

WASI Genetic Algorithm Runner

```zig
const std = @import("std");
const math = @import("std").math;
const ArrayList = std.ArrayList;
const Random = std.Random;
const native_endian = @import("builtin").target.cpu.arch.endian();

const POPULATION_SIZE: comptime_int = 100;
const GENES = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890, .-;:_!\"#%&/()=?@${[]}";
const MAX_TARGET_LENGTH: comptime_int = 50;
const MAX_GENERATIONS: comptime_int = 5_000; // Prevent infinite loops
const MAX_TRIALS: comptime_int = 32;
const GENE_MIX: comptime_float = 0.45;
const TOURNAMENT_SIZE = 10;

var target: []u8 = undefined;

var wasm_allocator = std.heap.wasm_allocator;

const Individual = struct {
string: []u8,
fitness: usize,

fn init(allocator: std.mem.Allocator, string: []const u8) !Individual {
const individual_string = try allocator.alloc(u8, string.len);
@memcpy(individual_string, string);

var individual = Individual{
.string = individual_string,
.fitness = 0,
};
return individual.calculateFitness();
}

fn deinit(self: *Individual, allocator: std.mem.Allocator) void {
allocator.free(self.string);
}

// mutate the Individual struct and compute the fitness:
// it is the number of differences between the genes and the target
fn calculateFitness(self: *Individual) Individual {
self.fitness = 0;
for (self.string, 0..) |char, i| {
if (char != target[i]) {
// Penalize more for being far from the correct character
// This creates a more informative gradient for selection
const target_char_index = std.mem.indexOfScalar(u8, GENES, target[i]) orelse GENES.len;
const individual_char_index = std.mem.indexOfScalar(u8, GENES, char) orelse GENES.len;

// Calculate distance between characters in the GENES string
const distance = @abs(@as(i32, @intCast(target_char_index)) - @as(i32, @intCast(individual_char_index)));

// Fitness is increased (worse) by the distance
self.fitness += distance + 1;
}
}

return self.*;
}

fn mate(
self: *const Individual,
allocator: std.mem.Allocator,
parent2: *const Individual,
ran: Random,
) !Individual {
const child_string = try allocator.alloc(u8, target.len);
for (child_string, 0..) |*gene, i| {
const p = ran.float(f32);
if (p < GENE_MIX) {
gene.* = self.string[i];
} else if (p < GENE_MIX * 2) {
gene.* = parent2.string[i];
} else {
gene.* = GENES[ran.intRangeAtMost(usize, 0, GENES.len - 1)];
}
}
// return try Individual.init(allocator, child_string);
return Individual{
.string = child_string,
.fitness = blk: {
var fitness: usize = 0;
for (child_string, 0..) |char, i| {
if (char != target[i]) {
const target_char_index = std.mem.indexOfScalar(u8, GENES, target[i]) orelse GENES.len;
const individual_char_index = std.mem.indexOfScalar(u8, GENES, char) orelse GENES.len;

const distance = @abs(@as(i32, @intCast(target_char_index)) - @as(i32, @intCast(individual_char_index)));

fitness += distance + 1;
}
}
break :blk fitness;
},
};
}
};

fn createGnome(allocator: std.mem.Allocator, len: usize, ran: Random) ![]u8 {
const gnome = try allocator.alloc(u8, len);
for (gnome) |*gene| {
gene.* = GENES[ran.intRangeAtMost(usize, 0, GENES.len - 1)];
}
return gnome;
}

fn individualLessThan(context: void, a: Individual, b: Individual) bool {
_ = context;
return a.fitness < b.fitness;
}

fn tournamentSelection(population: []Individual, tournament_size: usize, ran: Random) *const Individual {
var best_individual = &population[ran.intRangeAtMost(usize, 0, population.len - 1)];

for (1..tournament_size) |_| {
const candidate = &population[ran.intRangeAtMost(usize, 0, population.len - 1)];
if (candidate.fitness < best_individual.fitness) {
best_individual = candidate;
}
}

return best_individual;
}

fn runGeneticAlgorithm(
allocator: std.mem.Allocator,
elitism: usize,
ran: Random,
) !usize {
var population = ArrayList(Individual).init(allocator);

defer {
for (population.items) |*individual| {
individual.deinit(allocator);
// allocator.free(individual.string);
}
population.deinit();
}
var new_generation = ArrayList(Individual).init(allocator);
defer {
for (new_generation.items) |*individual| {
individual.deinit(allocator);
}
new_generation.deinit();
}
// Create initial population
for (0..POPULATION_SIZE) |_| {
const gnome = try createGnome(allocator, target.len, ran);
defer allocator.free(gnome);
const individual = try Individual.init(allocator, gnome);
try population.append(individual);
}

var generation: usize = 0;

while (generation < MAX_GENERATIONS) : (generation += 1) {
// Sort population by fitness
std.mem.sort(Individual, population.items, {}, individualLessThan);

// Check if we've found the target
if (population.items[0].fitness == 0) {
return generation;
}

// Generate new population

// Elitism
const elitism_count = @as(usize, (elitism * POPULATION_SIZE) / 100);
for (population.items[0..elitism_count]) |individual| {
try new_generation.append(try Individual.init(allocator, individual.string));
}

// Mating
const mating_count = POPULATION_SIZE - elitism_count;
for (0..mating_count) |_| {
// const parent1 = &population.items[ran.intRangeAtMost(usize, 0, 50)];
// const parent2 = &population.items[ran.intRangeAtMost(usize, 0, 50)];
const parent1 = tournamentSelection(
population.items,
TOURNAMENT_SIZE,
ran,
);
const parent2 = tournamentSelection(
population.items,
TOURNAMENT_SIZE,
ran,
);
const offspring = try parent1.mate(allocator, parent2, ran);
try new_generation.append(offspring);
}

// Replace old population
std.mem.swap(ArrayList(Individual), &population, &new_generation);
for (new_generation.items) |*individual| {
individual.deinit(allocator);
}

new_generation.clearRetainingCapacity();
}
// If solution not found
return MAX_GENERATIONS;
}

const RunningStats = struct {
mean: f64,
std_dev: f64,
min: usize,
max: usize,

fn init() RunningStats {
return RunningStats{
.mean = 0,
.std_dev = 0,
.min = std.math.maxInt(u32),
.max = 0,
};
}

fn calc_mean(self: *RunningStats, counts: []usize) void {
var sum: f64 = 0;
for (counts) |count| {
sum += @as(f64, @floatFromInt(count));
}
self.mean = sum / @as(f64, @floatFromInt(counts.len));
}
fn calc_std_dev_min_max(self: *RunningStats, counts: []usize) void {
if (counts.len < 2) return;
if (self.min == 0) self.min = counts[0];
if (self.max == 0) self.max = counts[0];
var sum: f64 = 0;

for (counts) |count| {
if (count > self.max) {
self.max = count;
}
if (count < self.min) {
self.min = count;
}
sum += std.math.pow(f64, (@as(f64, @floatFromInt(count)) - self.mean), 2);
}
self.std_dev = std.math.sqrt(sum / @as(f64, @floatFromInt(counts.len - 1)));
}

// mutate the Individual struct
fn calc(self: *RunningStats, counts: []usize) void {
self.calc_mean(counts);
self.calc_std_dev_min_max(counts);
}
};

// Freeing the allocated buffer
export fn free(ptr: [*]u8, len: usize) void {
wasm_allocator.free(ptr[0..len]);
}

const MetaResult = struct {
elitism: usize,
mean: f64,
std_dev: f64,
min: usize,
max: usize,
len: usize,
// target_length: usize,
// target_first_bytes: [8]u8 = [_]u8{0} ** 8,
};

fn serialise(meta_result: *MetaResult, allocator: std.mem.Allocator) ![]u8 {
const buffer = allocator.alloc(u8, 8 * 6) catch {
@panic("Could not allocate buffer");
};
errdefer allocator.free(buffer);
var stream = std.io.fixedBufferStream(buffer);
const writer = stream.writer();
try writer.writeInt(u64, @as(u64, @intCast(meta_result.elitism)), native_endian);
try writer.writeAll(std.mem.asBytes(&meta_result.mean));
try writer.writeAll(std.mem.asBytes(&meta_result.std_dev)); // f64
try writer.writeInt(u64, @as(u64, @intCast(meta_result.min)), .little);
try writer.writeInt(u64, @as(u64, @intCast(meta_result.max)), native_endian);
try writer.writeInt(u64, @as(u64, @intCast(meta_result.len)), native_endian);

// try writer.writeInt(u64, @as(u64, @intCast(meta_result.target_length)), native_endian);
// try writer.writeAll(meta_result.target_first_bytes[0..8]);

return buffer;
}

export fn alloc(len: usize) ?[*]u8 {
if (len > MAX_TARGET_LENGTH) {
return null;
}
target = wasm_allocator.alloc(u8, len) catch {
return null;
};
return target.ptr;
}

// debug function
export fn get_target() ?[*]u8 {
return target.ptr;
}

export fn set_target(ptr: [*]u8, len: usize) void {
@memcpy(target, ptr[0..len]);
}

export fn run(elitism: usize) [*]u8 {
var rand = Random.DefaultPrng.init(12345);
const ran = rand.random();
var generations = std.ArrayList(usize).init(wasm_allocator);
defer generations.deinit();
defer wasm_allocator.free(target);

for (0..MAX_TRIALS) |_| {
const count = runGeneticAlgorithm(wasm_allocator, elitism, ran) catch {
@panic("Could not run genetic algorithm");
};
generations.append(count) catch {
@panic("Could not append count to generations");
};
}
var stats = RunningStats.init();
const len = generations.items.len;
const results = generations.toOwnedSlice() catch {
@panic("Could not convert generations to owned slice");
};
defer wasm_allocator.free(results); //
stats.calc(results);

var meta_array = MetaResult{
.elitism = elitism,
.mean = stats.mean,
.std_dev = stats.std_dev,
.min = stats.min,
.max = stats.max,
.len = len,
// .target_length = target.len,
// .target_first_bytes = blk: {
// var first_bytes: [8]u8 = undefined;
// @memcpy(&first_bytes, target[0..8]);
// break :blk first_bytes;
// },
};

const serialized = serialise(&meta_array, wasm_allocator) catch {
@panic("Could not serialise meta results");
};
return serialized.ptr;
}

```


The Zig code of the NIF machine.



Threaded NIF Genetic Algorithm Runner

```zig
const beam = @import("beam");
const std = @import("std");
const math = std.math;
const print = std.debug.print;
const ArrayList = std.ArrayList;
const Random = std.Random;
const Thread = std.Thread;
const native_endian = @import("builtin").target.cpu.arch.endian();

const POPULATION_SIZE: comptime_int = 100;
const GENES = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890, .-;:_!\"#%&/()=?@${[]}";
const MAX_TARGET_LENGTH: comptime_int = 50;
const MAX_GENERATIONS: comptime_int = 5_000; // Prevent infinite loops
const TOURNAMENT_SIZE = 10;
const GENE_MIX: comptime_float = 0.45;
const NB_TRIALS_PER_THREAD: comptime_int = 4;

var beam_allocator = beam.allocator;

const ThreadContext = struct {
allocator: std.mem.Allocator,
elitism: usize,
results_ptr: []usize,
input: []const u8,
};

// New ThreadPool structure for managing parallel trials
const ThreadPool = struct {
allocator: std.mem.Allocator,
threads: []Thread,
results: []usize,
elitism: usize,

fn init(args: struct {
allocator: std.mem.Allocator,
num_threads: usize,
elitism: usize,
}) !ThreadPool {
const results = try args.allocator.alloc(usize, args.num_threads * NB_TRIALS_PER_THREAD);
const threads = try args.allocator.alloc(Thread, args.num_threads);

return ThreadPool{
.allocator = args.allocator,
.threads = threads,
.results = results,
.elitism = args.elitism,
};
}

fn deinit(self: *ThreadPool) void {
self.allocator.free(self.threads);
self.allocator.free(self.results);
}

fn runParallelTrials(self: *ThreadPool, input: []const u8) !void {
for (0..self.threads.len) |i| {
// we build a slice of the results array for each thread
const thread_results = self.results[i * NB_TRIALS_PER_THREAD .. (i + 1) * NB_TRIALS_PER_THREAD];

const thread_context = ThreadContext{
.allocator = self.allocator,
.elitism = self.elitism,
.results_ptr = thread_results, // a slice
.input = input,
};

self.threads[i] = try Thread.spawn(
.{},
threadTrialRunner,
.{thread_context},
);
}

// Join all threads
for (self.threads) |*thread| {
thread.join();
}
}
};

fn threadTrialRunner(context: ThreadContext) !void {
var rand = Random.DefaultPrng.init(@as(u64, @intCast(std.time.timestamp())));
const ran = rand.random();

for (0..NB_TRIALS_PER_THREAD) |j| {
const trial_count = try runGeneticAlgorithm(
context.allocator,
context.elitism,
ran,
context.input,
);
// updates the slice
context.results_ptr[j] = @min(MAX_GENERATIONS, trial_count);
}
}

const Individual = struct {
string: []u8,
fitness: usize,

fn init(allocator: std.mem.Allocator, string: []const u8, fitness: ?usize) !Individual {
const individual_string = try allocator.alloc(u8, string.len);
@memcpy(individual_string, string);

return Individual{
.string = individual_string,
.fitness = fitness orelse 0,
// std.math.maxInt(u32),
};
}

// The distance is calculated based on the difference in position
// between the characters in the GENES string and the target string.
fn calculateFitness(self: *Individual, target: []const u8) void {
// self.fitness = 0;
// for (self.string, 0..) |char, i| {
// if (char != target[i]) {
// self.fitness += 1;
// }
// }
// return self.*;
self.fitness = 0;
for (self.string, 0..) |char, i| {
if (char != target[i]) {
// Penalize more for being far from the correct character
// This creates a more informative gradient for selection
const target_char_index = std.mem.indexOfScalar(u8, GENES, target[i]) orelse GENES.len;
const individual_char_index = std.mem.indexOfScalar(u8, GENES, char) orelse GENES.len;

// Calculate distance between characters in the GENES string
const distance = @abs(@as(i32, @intCast(target_char_index)) - @as(i32, @intCast(individual_char_index)));

// Fitness is increased (worse) by the distance
self.fitness += distance + 1;
}
}
}

fn deinit(self: *Individual, allocator: std.mem.Allocator) void {
allocator.free(self.string);
}

fn mate(
self: *const Individual,
allocator: std.mem.Allocator,
parent2: *const Individual,
ran: Random,
target: []const u8,
) !Individual {
const child_string = try allocator.alloc(u8, target.len);
for (child_string, 0..) |*gene, i| {
const p = ran.float(f32);
if (p < GENE_MIX) {
gene.* = self.string[i];
} else if (p < GENE_MIX * 2) {
gene.* = parent2.string[i];
} else {
gene.* = GENES[ran.intRangeAtMost(usize, 0, GENES.len - 1)];
}
}
return Individual{
.string = child_string,
.fitness = blk: {
var fitness: usize = 0;
for (child_string, 0..) |char, i| {
if (char != target[i]) {
const target_char_index = std.mem.indexOfScalar(u8, GENES, target[i]) orelse GENES.len;
const individual_char_index = std.mem.indexOfScalar(u8, GENES, char) orelse GENES.len;

const distance = @abs(@as(i32, @intCast(target_char_index)) - @as(i32, @intCast(individual_char_index)));

fitness += distance + 1;
}
}
break :blk fitness;
},
};
}
};

fn createGnome(allocator: std.mem.Allocator, len: usize, ran: Random) ![]u8 {
const gnome = try allocator.alloc(u8, len);
for (gnome) |*gene| {
gene.* = GENES[
ran.intRangeAtMost(
usize,
0,
GENES.len - 1,
)
];
}
return gnome;
}

fn individualLessThan(context: void, a: Individual, b: Individual) bool {
_ = context;
return a.fitness < b.fitness;
}

fn tournamentSelection(population: []Individual, tournament_size: usize, ran: Random) *const Individual {
var best_individual = &population[
ran.intRangeAtMost(
usize,
0,
population.len - 1,
)
];

for (1..tournament_size) |_| {
const candidate = &population[
ran.intRangeAtMost(
usize,
0,
population.len - 1,
)
];
if (candidate.fitness < best_individual.fitness) {
best_individual = candidate;
}
}

return best_individual;
}

const RunningStats = struct {
mean: f64,
std_dev: f64,
min: usize,
max: usize,

fn init() RunningStats {
return RunningStats{
.mean = 0,
.std_dev = 0,
.min = std.math.maxInt(u32),
.max = 0,
};
}

fn calc_mean(self: *RunningStats, counts: []usize) void {
var sum: f64 = 0;
for (counts) |count| {
sum += @as(f64, @floatFromInt(count));
}
self.mean = sum / @as(f64, @floatFromInt(counts.len));
}
fn calc_std_dev_min_max(self: *RunningStats, counts: []usize) void {
if (counts.len < 2) return;
if (self.min == 0) self.min = counts[0];
if (self.max == 0) self.max = counts[0];
var sum: f64 = 0;

for (counts) |count| {
if (count > self.max) {
self.max = count;
}
if (count < self.min) {
self.min = count;
}
sum += std.math.pow(f64, (@as(f64, @floatFromInt(count)) - self.mean), 2);
}
self.std_dev = std.math.sqrt(sum / @as(f64, @floatFromInt(counts.len - 1)));
}

// mutate the Individual struct
fn calc(self: *RunningStats, counts: []usize) void {
self.calc_mean(counts);
self.calc_std_dev_min_max(counts);
}
};

const MetaResult = struct {
elitism: usize,
mean: f64,
std_dev: f64,
min: usize,
max: usize,
};

fn runGeneticAlgorithm(
allocator: std.mem.Allocator,
elitism: usize,
ran: Random,
input: []const u8,
) !usize {
var population = std.ArrayList(Individual).init(allocator);
defer {
for (population.items) |*individual| {
individual.deinit(allocator);
}
population.deinit();
}

var new_generation = std.ArrayList(Individual).init(allocator);
defer {
for (new_generation.items) |*individual| {
individual.deinit(allocator);
}
new_generation.deinit();
}

// Create initial population
for (0..POPULATION_SIZE) |_| {
const gnome = try createGnome(
allocator,
input.len,
ran,
);
defer allocator.free(gnome);

var individual = try Individual.init(
allocator,
gnome,
null,
);

individual.calculateFitness(input);
try population.append(individual);
}

var generation: usize = 0;

while (generation < MAX_GENERATIONS) : (generation += 1) {
// Sort population by fitness
std.mem.sort(
Individual,
population.items,
{},
individualLessThan,
);

// End condition: check if we've found the target
if (population.items[0].fitness == 0) {
return generation;
}

// Generate new population

// Copy top individuals into new generation
const elitism_count = @as(usize, (elitism * POPULATION_SIZE) / 100);
for (population.items[0..elitism_count]) |individual| {
try new_generation.append(try Individual.init(
allocator,
individual.string,
individual.fitness,
));
}

// Mating: fill the rest of the new generation with an offsptring from a tournament
const mating_count = POPULATION_SIZE - elitism_count;
for (0..mating_count) |_| {
// const parent1 = &population.items[ran.intRangeAtMost(usize, 0, 50)];
// const parent2 = &population.items[ran.intRangeAtMost(usize, 0, 50)];
const parent1 = tournamentSelection(
population.items,
TOURNAMENT_SIZE,
ran,
);
const parent2 = tournamentSelection(
population.items,
TOURNAMENT_SIZE,
ran,
);
const offspring = try parent1.mate(
allocator,
parent2,
ran,
input,
);
try new_generation.append(offspring);
}

for (population.items) |*individual| {
individual.deinit(allocator);
}

// Replace old population
std.mem.swap(ArrayList(Individual), &population, &new_generation);
new_generation.clearRetainingCapacity();
}
return MAX_GENERATIONS;
}

/// nif: run/2 Threaded
pub fn run(elitism: usize, input: []const u8) !MetaResult {
const num_cpus = Thread.getCpuCount() catch 2;

var thread_pool = try ThreadPool.init(.{
.allocator = beam_allocator,
.num_threads = num_cpus,
.elitism = elitism,
});

defer thread_pool.deinit();

try thread_pool.runParallelTrials(input);

var stats = RunningStats.init();
stats.calc(thread_pool.results);

const meta_array = MetaResult{
.elitism = elitism,
.mean = stats.mean,
.std_dev = stats.std_dev,
.min = stats.min,
.max = stats.max,
};

// print("MetaResult: elitism={}, mean={}, std_dev={}, min={}, max={}\n", .{
// meta_array.elitism,
// meta_array.mean,
// meta_array.std_dev,
// meta_array.min,
// meta_array.max,
// });

return .{
.elitism = meta_array.elitism,
.mean = meta_array.mean,
.std_dev = meta_array.std_dev,
.min = meta_array.min,
.max = meta_array.max,
};
}

```


Unthreaded NIF Genetic Algorithm Runner

```zig
const beam = @import("beam");
const std = @import("std");
const math = std.math;
const ArrayList = std.ArrayList;
const Random = std.Random;
const native_endian = @import("builtin").target.cpu.arch.endian();

const POPULATION_SIZE: comptime_int = 100;
const GENES = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ 1234567890, .-;:_!\"#%&/()=?@${[]}";
const MAX_TARGET_LENGTH: comptime_int = 50;
const MAX_GENERATIONS: comptime_int = 5_000; // Prevent infinite loops
const TOURNAMENT_SIZE = 10;
const GENE_MIX: comptime_float = 0.45;
// const NB_TRIALS_PER_THREAD: comptime_int = 4;
const MAX_TRIALS: comptime_int = 32;

var beam_allocator = beam.allocator;

const Individual = struct {
string: []u8,
fitness: usize,

fn init(allocator: std.mem.Allocator, string: []const u8, fitness: ?usize) !Individual {
const individual_string = try allocator.alloc(u8, string.len);
@memcpy(individual_string, string);

return Individual{
.string = individual_string,
.fitness = fitness orelse 0,
// std.math.maxInt(u32),
};
}

// The distance is calculated based on the difference in position
// between the characters in the GENES string and the target string.
fn calculateFitness(self: *Individual, target: []const u8) void {
self.fitness = 0;
for (self.string, 0..) |char, i| {
if (char != target[i]) {
// Penalize more for being far from the correct character
// This creates a more informative gradient for selection
const target_char_index = std.mem.indexOfScalar(u8, GENES, target[i]) orelse GENES.len;
const individual_char_index = std.mem.indexOfScalar(u8, GENES, char) orelse GENES.len;

// Calculate distance between characters in the GENES string
const distance = @abs(@as(i32, @intCast(target_char_index)) - @as(i32, @intCast(individual_char_index)));

// Fitness is increased (worse) by the distance
self.fitness += distance + 1;
}
}
// return self.*;
}

fn deinit(self: *Individual, allocator: std.mem.Allocator) void {
allocator.free(self.string);
}

fn mate(
self: *const Individual,
allocator: std.mem.Allocator,
parent2: *const Individual,
ran: Random,
target: []const u8,
) !Individual {
const child_string = try allocator.alloc(u8, target.len);
for (child_string, 0..) |*gene, i| {
const p = ran.float(f32);
if (p < GENE_MIX) {
gene.* = self.string[i];
} else if (p < GENE_MIX * 2) {
gene.* = parent2.string[i];
} else {
gene.* = GENES[ran.intRangeAtMost(usize, 0, GENES.len - 1)];
}
}
return Individual{
.string = child_string,
.fitness = blk: {
var fitness: usize = 0;
for (child_string, 0..) |char, i| {
if (char != target[i]) {
const target_char_index = std.mem.indexOfScalar(u8, GENES, target[i]) orelse GENES.len;
const individual_char_index = std.mem.indexOfScalar(u8, GENES, char) orelse GENES.len;

const distance = @abs(@as(i32, @intCast(target_char_index)) - @as(i32, @intCast(individual_char_index)));

fitness += distance + 1;
}
}
break :blk fitness;
},
};
}
};

fn createGnome(allocator: std.mem.Allocator, len: usize, ran: Random) ![]u8 {
const gnome = try allocator.alloc(u8, len);
for (gnome) |*gene| {
gene.* = GENES[
ran.intRangeAtMost(
usize,
0,
GENES.len - 1,
)
];
}
return gnome;
}

fn individualLessThan(context: void, a: Individual, b: Individual) bool {
_ = context;
return a.fitness < b.fitness;
}

fn tournamentSelection(population: []Individual, tournament_size: usize, ran: Random) *const Individual {
var best_individual = &population[
ran.intRangeAtMost(
usize,
0,
population.len - 1,
)
];

for (1..tournament_size) |_| {
const candidate = &population[
ran.intRangeAtMost(
usize,
0,
population.len - 1,
)
];
if (candidate.fitness < best_individual.fitness) {
best_individual = candidate;
}
}

return best_individual;
}

fn runGeneticAlgorithm(
allocator: std.mem.Allocator,
elitism: usize,
ran: Random,
input: []const u8,
) !usize {
var population = std.ArrayList(Individual).init(allocator);
defer {
for (population.items) |*individual| {
individual.deinit(allocator);
}
population.deinit();
}

var new_generation = std.ArrayList(Individual).init(allocator);
defer {
for (new_generation.items) |*individual| {
individual.deinit(allocator);
}
new_generation.deinit();
}

// Create initial population
for (0..POPULATION_SIZE) |_| {
const gnome = try createGnome(
allocator,
input.len,
ran,
);
defer allocator.free(gnome);

var individual = try Individual.init(
allocator,
gnome,
null,
);

individual.calculateFitness(input);
try population.append(individual);
}

var generation: usize = 0;

while (generation < MAX_GENERATIONS) : (generation += 1) {
// Sort population by fitness
std.mem.sort(
Individual,
population.items,
{},
individualLessThan,
);

// End condition: check if we've found the target
if (population.items[0].fitness == 0) {
return generation;
}

// Generate new population

// Copy top individuals into new generation
const elitism_count = @as(usize, (elitism * POPULATION_SIZE) / 100);
for (population.items[0..elitism_count]) |individual| {
try new_generation.append(try Individual.init(
allocator,
individual.string,
individual.fitness,
));
}

// Mating: fill the rest of the new generation with an offsptring from a tournament
const mating_count = POPULATION_SIZE - elitism_count;
for (0..mating_count) |_| {
// const parent1 = &population.items[ran.intRangeAtMost(usize, 0, 50)];
// const parent2 = &population.items[ran.intRangeAtMost(usize, 0, 50)];
const parent1 = tournamentSelection(
population.items,
TOURNAMENT_SIZE,
ran,
);
const parent2 = tournamentSelection(
population.items,
TOURNAMENT_SIZE,
ran,
);
const offspring = try parent1.mate(
allocator,
parent2,
ran,
input,
);
try new_generation.append(offspring);
}

for (population.items) |*individual| {
individual.deinit(allocator);
}

// Replace old population
std.mem.swap(ArrayList(Individual), &population, &new_generation);
new_generation.clearRetainingCapacity();
}
return MAX_GENERATIONS;
}

const RunningStats = struct {
mean: f64,
std_dev: f64,
min: usize,
max: usize,

fn init() RunningStats {
return RunningStats{
.mean = 0,
.std_dev = 0,
.min = std.math.maxInt(u32),
.max = 0,
};
}

fn calc_mean(self: *RunningStats, counts: []usize) void {
var sum: f64 = 0;
for (counts) |count| {
sum += @as(f64, @floatFromInt(count));
}
self.mean = sum / @as(f64, @floatFromInt(counts.len));
}
fn calc_std_dev_min_max(self: *RunningStats, counts: []usize) void {
if (counts.len < 2) return;
if (self.min == 0) self.min = counts[0];
if (self.max == 0) self.max = counts[0];
var sum: f64 = 0;

for (counts) |count| {
if (count > self.max) {
self.max = count;
}
if (count < self.min) {
self.min = count;
}
sum += std.math.pow(f64, (@as(f64, @floatFromInt(count)) - self.mean), 2);
}
self.std_dev = std.math.sqrt(sum / @as(f64, @floatFromInt(counts.len - 1)));
}

// mutate the Individual struct
fn calc(self: *RunningStats, counts: []usize) void {
self.calc_mean(counts);
self.calc_std_dev_min_max(counts);
}
};

const MetaResult = struct {
elitism: usize,
mean: f64,
std_dev: f64,
min: usize,
max: usize,
};

pub fn run(elitism: usize, input: []const u8) !MetaResult {
var rand = Random.DefaultPrng.init(@intCast(std.time.timestamp()));
const ran = rand.random();

var generations = std.ArrayList(usize).init(beam_allocator);
defer generations.deinit();

for (0..MAX_TRIALS) |_| {
const count = try runGeneticAlgorithm(beam_allocator, elitism, ran, input);
try generations.append(count);
}
var stats = RunningStats.init();
const results = try generations.toOwnedSlice();
stats.calc(results);

const meta_array = MetaResult{
.elitism = elitism,
.mean = stats.mean,
.std_dev = stats.std_dev,
.min = stats.min,
.max = stats.max,
};

return .{
.elitism = meta_array.elitism,
.mean = meta_array.mean,
.std_dev = meta_array.std_dev,
.min = meta_array.min,
.max = meta_array.max,
};
}

```

Contributor guide

No contributing guide indexed for this repository

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.