Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ziggy Nif vs WASI vs WASM- speed comparison #18

Open
ndrean opened this issue Dec 17, 2024 · 2 comments
Open

Ziggy Nif vs WASI vs WASM- speed comparison #18

ndrean opened this issue Dec 17, 2024 · 2 comments

Comments

@ndrean
Copy link

ndrean commented Dec 17, 2024

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
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:

# 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?

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])

    <<elitism::unsigned-little-64, mean::float-little-64, std_dev::float-little-64,
      min::unsigned-little-64, max::unsigned-little-64>> = 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
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
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
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,
    };
}
@ndrean
Copy link
Author

ndrean commented Dec 18, 2024

Since WebAssembly is primarily made to run in the browser, let's run it in the browser.

We find that it is comparable to the Wasi version run on the server (at least on my - very old - computer).

4_952.399 ms
{
    "elitism": 10,
    "mean": 1086.9375,
    "stdDev": 451.45,
    "min": 428,
    "max": 2668,
    "len": 32
}

You need to pass a string from the host (Javascript) to the WebAssembly container, and recover data from the WAC.

  • you call a Zig function that allocates memory of a given length

  • you populate the WA memory at this position (via an ArrayBuffer given by TextDecoder with "ascii" encoding)

  • you call a Zig function to read the memory at this position and populate an internal variable

  • to recover data from Zig, you need to serialise it into a linear format

  • you call a Zig function that sends you the index of this data

  • you populate a DataView at this index and convert the data

HTML
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <script type="module">
      const { initializeWasmModule } = await import("./index.js");
      const { setInput, run } = await initializeWasmModule();

      console.log("starting");
      const t0 = performance.now();

      setInput("I want a beer! What about you?!");
      const results = run(10);
      const t1 = performance.now();
      console.log(t1 - t0, results);
    </script>
  </body>
</html>

index.js
export async function initializeWasmModule() {
  const response = await fetch("ga_browser.wasm");
  const binary = await response.arrayBuffer();

  const memory = new WebAssembly.Memory({ initial: 512 });
  const { instance } = await WebAssembly.instantiate(binary, {
    env: { memory },
  });

  return {
    setInput: (input) => {
      const { memory, set_target, alloc } = instance.exports;

      const len = input.length;
      const stringBuffer = new TextEncoder("ascii").encode(input);

      const ptr = alloc(len); // Zig alcator
      if (ptr < memory.buffer.byteLength && ptr > 0) {
        const wasmMemory = new Uint8Array(memory.buffer);
        wasmMemory.set(stringBuffer, ptr);

        set_target(ptr, len); // Zig set_target
      } else {
        console.error("Failed to allocate memory");
      }
    },

    run: (elitism = 10) => {
      const { run, memory, free } = instance.exports;

      const resultPtr = run(elitism);
      const view = new DataView(memory.buffer, resultPtr, 8 * 6);

      const result = {
        elitism: Number(view.getBigUint64(0, true)),
        mean: view.getFloat64(8, true),
        stdDev: view.getFloat64(16, true),
        min: Number(view.getBigUint64(24, true)),
        max: Number(view.getBigUint64(32, true)),
        len: Number(view.getBigUint64(40, true)),
        // targetLength: Number(view.getBigUint64(48, true)),
        // targetFirstBytes: Array.from(
        //   new Uint8Array(memory.buffer, resultPtr + 7 * 8, 8)
        // )
        //   .map((b) => String.fromCharCode(b))
        //   .join(""),
      };

      free(resultPtr, 64);
      return result;
    },
  };
}

build Zig to WASM
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 = .freestanding,
    });

    // 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.initial_memory = 512 * std.wasm.page_size; // 512 pages of 64KiB
    b.installArtifact(exe);
}

WASM 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;
}

@ndrean ndrean changed the title Ziggy Nif vs WASI - speed comparison Ziggy Nif vs WASI vs WASM- speed comparison Dec 18, 2024
@ndrean
Copy link
Author

ndrean commented Dec 19, 2024

A side note. When I tried to use the WebAssembly container I used successfully server-side in the browser, I encounter a vicious error.
I used std.time.timestamp() to generate random numbers, deep in the code, and forgot about it. I had an error related to posix. However, a container knows nothing about the host, so no such thing as I/O.... so use a seed like "12345" instead!

Imports. You can import console.log into the container with WebAssemby.Module.imports.

Consequence: it might be easier to run the container in the browser using console.log along the road for debugging. When its ready, you can use it with Elixir, given that you mirror the function calls with Wasmex (settting pointers/index etc).

Web Worker thread . To use a WASM in a Web Worker. you compile the module in your main thread, and pass the binary as a postMessage to the worker. You can now use it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant