Created
May 13, 2020 18:21
-
-
Save andrewrk/f64a232fc122cf62eec29c341fc9c7c2 to your computer and use it in GitHub Desktop.
RedisConf2020 Talk: "Writing Redis Modules in Zig" https://www.youtube.com/watch?v=Csz26Wy9Ses
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
test "benchmark" { | |
var timer = try std.time.Timer.start(); | |
const start = timer.lap(); | |
var i: usize = 0; | |
var hash: usize = 0; | |
while (i < 1000000) : (i += 1) { | |
const src = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>."; | |
var output: [100]u8 = undefined; | |
var bf: BrainFuckInterpreter = undefined; | |
bf.init(src, "", &output, 500); | |
bf.start(); | |
for (bf.output[0..bf.output_index]) |byte| { | |
hash +%= byte; | |
} | |
} | |
const end = timer.lap(); | |
const elapsed_s = @intToFloat(f64, end - start) / std.time.ns_per_s; | |
std.debug.warn("time={} hash={}\n", .{ elapsed_s, hash }); | |
} | |
#include <time.h> | |
int main(int argc, char **argv) { | |
struct timespec start; | |
clock_gettime(CLOCK_MONOTONIC, &start); | |
size_t hash = 0; | |
for (size_t i = 0; i < 1000000; i += 1) { | |
const char *src = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>."; | |
char output[100]; | |
struct BrainFuckInterpreter bf; | |
BrainFuckInterpreter_init(&bf, src, strlen(src), "", 0, output, 100, 500); | |
BrainFuckInterpreter_start(&bf); | |
for (size_t j = 0; j < bf.output_index; j += 1) { | |
hash += bf.output_ptr[j]; | |
} | |
} | |
struct timespec end; | |
clock_gettime(CLOCK_MONOTONIC, &end); | |
double start_ns = start.tv_sec * 1000000000.0 + start.tv_nsec; | |
double end_ns = end.tv_sec * 1000000000.0 + end.tv_nsec; | |
double elapsed_s = (end_ns - start_ns) / 1000000000.0; | |
fprintf(stderr, "time=%f hash=%zu\n", elapsed_s, hash); | |
} | |
andy@ark ~/m/R/Talk-WritingRedisModulesInZig> gcc --version | |
gcc (GCC) 9.2.0 | |
andy@ark ~/m/R/Talk-WritingRedisModulesInZig> gcc -o bench_c bf.c -fPIC -std=gnu99 -O2 | |
andy@ark ~/m/R/Talk-WritingRedisModulesInZig> ./bench_c | |
time=1.302713 hash=1095000000 | |
andy@ark ~/m/R/Talk-WritingRedisModulesInZig> clang --version | |
clang version 7.1.0 (tags/RELEASE_710/final) | |
andy@ark ~/m/R/Talk-WritingRedisModulesInZig> ./bench_c | |
time=1.147775 hash=1095000000 | |
andy@ark ~/m/R/Talk-WritingRedisModulesInZig> zig version | |
0.6.0 | |
andy@ark ~/m/R/Talk-WritingRedisModulesInZig> zig test bf.zig -lc -I. --release-fast | |
Test [2/2] test "benchmark"...time=1.072691358e+00 hash=1095000000 | |
andy@ark ~/m/R/Talk-WritingRedisModulesInZig> zig cc -o bench_c bf.c -fPIC -std=gnu99 -O2 | |
andy@ark ~/m/R/Talk-WritingRedisModulesInZig> ./bench_c | |
time=1.052307 hash=1095000000 | |
>>> (1.302713 - 1.147775) / 1.302713 | |
0.11893486900030936 | |
>>> (1.302713 - 1.072691358) / 1.302713 | |
0.17657123403236175 | |
>>> (1.302713 - 1.052307) / 1.302713 | |
0.19221885403768896 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "redismodule.h" | |
#include <string.h> | |
#define TAPE_SIZE (8 * 1024) | |
#define MAX_PRG_BYTES 400 | |
struct BrainFuckInterpreter { | |
char tape[TAPE_SIZE]; | |
size_t tape_head; | |
size_t pc; | |
size_t cycle_count; | |
const char *program_ptr; | |
size_t program_len; | |
size_t max_cycles; | |
size_t matching_bracket[MAX_PRG_BYTES]; | |
char *output_ptr; | |
size_t output_len; | |
size_t output_index; | |
const char *input_ptr; | |
size_t input_len; | |
size_t input_index; | |
}; | |
static struct BrainFuckInterpreter BrainFuckInterpreter_init( | |
const char *src_ptr, size_t src_len, | |
const char *input_ptr, size_t input_len, | |
char *output_ptr, size_t output_len, | |
size_t max_cycles) | |
{ | |
struct BrainFuckInterpreter bf; | |
memset(&bf.tape, 0, TAPE_SIZE); | |
bf.tape_head = 0; | |
bf.pc = 0; | |
bf.cycle_count = 0; | |
bf.program_ptr = src_ptr; | |
bf.program_len = src_len; | |
bf.max_cycles = max_cycles; | |
bf.input_ptr = input_ptr; | |
bf.input_len = input_len; | |
bf.input_index = 0; | |
bf.output_ptr = output_ptr; | |
bf.output_len = output_len; | |
bf.output_index = 0; | |
// parse for matching brackets | |
for (size_t i = 0; i < src_len; i += 1) { | |
// default: no control flow modification | |
bf.matching_bracket[i] = i; | |
} | |
size_t stack[MAX_PRG_BYTES]; | |
size_t stack_index = 0; | |
for (size_t i = 0; i < src_len; i += 1) { | |
switch (src_ptr[i]) { | |
case '[': | |
stack[stack_index] = i; | |
stack_index += 1; | |
continue; | |
case ']': | |
if (stack_index != 0) { | |
stack_index -= 1; | |
size_t begin_index = stack[stack_index]; | |
bf.matching_bracket[i] = begin_index; | |
bf.matching_bracket[begin_index] = i; | |
} | |
continue; | |
default: | |
continue; | |
} | |
} | |
return bf; | |
} | |
static void BrainFuckInterpreter_start(struct BrainFuckInterpreter *bf) { | |
while (bf->pc < bf->program_len) { | |
if (bf->cycle_count >= bf->max_cycles) return; | |
switch (bf->program_ptr[bf->pc]) { | |
case '<': | |
if (bf->tape_head != 0) | |
bf->tape_head -= 1; | |
break; | |
case '>': | |
if (bf->tape_head < TAPE_SIZE) | |
bf->tape_head += 1; | |
break; | |
case '+': | |
bf->tape[bf->tape_head] += 1; | |
break; | |
case '-': | |
bf->tape[bf->tape_head] -= 1; | |
break; | |
case '.': | |
if (bf->output_index < bf->output_len) { | |
bf->output_ptr[bf->output_index] = bf->tape[bf->tape_head]; | |
bf->output_index += 1; | |
} | |
break; | |
case ',': | |
if (bf->input_index >= bf->input_len) { | |
bf->tape[bf->tape_head] = 0; | |
} else { | |
bf->tape[bf->tape_head] = bf->input_ptr[bf->input_index]; | |
bf->input_index += 1; | |
} | |
break; | |
case '[': | |
if (bf->tape[bf->tape_head] == 0) { | |
bf->pc = bf->matching_bracket[bf->pc]; | |
} | |
break; | |
case ']': | |
if (bf->matching_bracket[bf->pc] != bf->pc) { | |
bf->pc = bf->matching_bracket[bf->pc]; | |
bf->cycle_count += 1; | |
continue; | |
} | |
break; | |
default: | |
break; | |
} | |
bf->pc += 1; | |
bf->cycle_count += 1; | |
} | |
} | |
int BF_Command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | |
if (argc < 2) | |
return RedisModule_WrongArity(ctx); | |
size_t src_len; | |
const char *src_ptr = RedisModule_StringPtrLen(argv[1], &src_len); | |
size_t max_cycles = 1000; | |
if (argc >= 3) { | |
long long requested_cycles; | |
if (RedisModule_StringToLongLong(argv[2], &requested_cycles) == REDISMODULE_ERR) | |
return REDISMODULE_ERR; | |
if (requested_cycles < 0) | |
return RedisModule_ReplyWithError(ctx, "invalid max cycles value"); | |
max_cycles = requested_cycles; | |
} | |
const char *input_ptr = ""; | |
size_t input_len = 0; | |
if (argc >= 4) | |
input_ptr = RedisModule_StringPtrLen(argv[3], &input_len); | |
unsigned int hash = 0; | |
for (size_t i = 0; i < src_len; i += 1) { | |
unsigned int random_bits = 0xfb5c4c07; | |
hash += (unsigned)src_ptr[i] * random_bits; | |
} | |
RedisModule_Log(ctx, "NOTICE", "program hash: 0x%x", hash); | |
char output_buf[1024]; | |
struct BrainFuckInterpreter bf = BrainFuckInterpreter_init( | |
src_ptr, src_len, input_ptr, input_len, | |
output_buf, sizeof(output_buf), max_cycles); | |
BrainFuckInterpreter_start(&bf); | |
if (bf.cycle_count == bf.max_cycles) | |
return RedisModule_ReplyWithError(ctx, "program exceeded maximum cycle count"); | |
return RedisModule_ReplyWithStringBuffer(ctx, bf.output_ptr, bf.output_index); | |
} | |
int RedisModule_OnLoad(RedisModuleCtx *ctx) { | |
if (RedisModule_Init(ctx, "bfmodule", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | |
return REDISMODULE_ERR; | |
if (RedisModule_CreateCommand(ctx, "bf", BF_Command, "readonly", 0, 0, 0) == REDISMODULE_ERR) | |
return REDISMODULE_ERR; | |
return REDISMODULE_OK; | |
} | |
#include <time.h> | |
int main(int argc, char **argv) { | |
struct timespec start; | |
clock_gettime(CLOCK_MONOTONIC, &start); | |
size_t hash = 0; | |
for (size_t i = 0; i < 1000000; i += 1) { | |
const char *src = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>."; | |
char output[100]; | |
struct BrainFuckInterpreter bf = BrainFuckInterpreter_init(src, strlen(src), "", 0, output, 100, 500); | |
BrainFuckInterpreter_start(&bf); | |
for (size_t j = 0; j < bf.output_index; j += 1) { | |
hash += bf.output_ptr[j]; | |
} | |
} | |
struct timespec end; | |
clock_gettime(CLOCK_MONOTONIC, &end); | |
double start_ns = start.tv_sec * 1000000000.0 + start.tv_nsec; | |
double end_ns = end.tv_sec * 1000000000.0 + end.tv_nsec; | |
double elapsed_s = (end_ns - start_ns) / 1000000000.0; | |
fprintf(stderr, "time=%f hash=%zu\n", elapsed_s, hash); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const std = @import("std"); | |
usingnamespace @cImport(@cInclude("redismodule.h")); | |
export fn BF_Command(ctx: ?*RedisModuleCtx, argv: ?[*]?*RedisModuleString, argc: c_int) c_int { | |
if (argc < 2) | |
return RedisModule_WrongArity.?(ctx); | |
var src_len: usize = undefined; | |
const src_ptr = RedisModule_StringPtrLen.?(argv.?[1], &src_len); | |
const src = src_ptr[0..src_len]; | |
const max_cycles = if (argc >= 3) blk: { | |
var requested_cycles: i64 = undefined; | |
if (RedisModule_StringToLongLong.?(argv.?[2], &requested_cycles) == REDISMODULE_ERR) | |
return REDISMODULE_ERR; | |
const cycles_usize = std.math.cast(usize, requested_cycles) catch |err| switch (err) { | |
error.Overflow => return RedisModule_ReplyWithError.?(ctx, "invalid max cycles value"), | |
}; | |
break :blk cycles_usize; | |
} else 1000; | |
const input = if (argc >= 4) blk: { | |
var len: usize = undefined; | |
const ptr = RedisModule_StringPtrLen.?(argv.?[3], &len); | |
break :blk ptr[0..len]; | |
} else ""; | |
var hash: u32 = 0; | |
for (src) |byte| { | |
const random_bits = 0xfb5c4c07; | |
hash +%= @as(u32, byte) *% random_bits; | |
} | |
RedisModule_Log.?(ctx, "NOTICE", "program hash: 0x%x", hash); | |
var output_buf: [1024]u8 = undefined; | |
var bf = BrainFuckInterpreter.init(src_ptr[0..src_len], input, &output_buf, max_cycles); | |
bf.start(); | |
if (bf.cycle_count == bf.max_cycles) | |
return RedisModule_ReplyWithError.?(ctx, "program exceeded maximum cycle count"); | |
const output = bf.output[0..bf.output_index]; | |
return RedisModule_ReplyWithStringBuffer.?(ctx, output.ptr, output.len); | |
} | |
export fn RedisModule_OnLoad(ctx: *RedisModuleCtx) c_int { | |
if (RedisModule_Init(ctx, "bfmodule", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | |
return REDISMODULE_ERR; | |
if (RedisModule_CreateCommand.?(ctx, "bf", BF_Command, "readonly", 0, 0, 0) == REDISMODULE_ERR) | |
return REDISMODULE_ERR; | |
return REDISMODULE_OK; | |
} | |
const tape_size = 8 * 1024; | |
const max_prg_bytes = 400; | |
const BrainFuckInterpreter = struct { | |
tape: [tape_size]u8, | |
tape_head: usize, | |
pc: usize, | |
cycle_count: usize, | |
program: []const u8, | |
max_cycles: usize, | |
matching_bracket: [max_prg_bytes]usize, | |
output: []u8, | |
output_index: usize, | |
input: []const u8, | |
input_index: usize, | |
pub fn init(program_bytes: []const u8, input: []const u8, output: []u8, max_cycles: usize) BrainFuckInterpreter { | |
var self: BrainFuckInterpreter = .{ | |
.tape = [1]u8{0} ** tape_size, | |
.tape_head = 0, | |
.matching_bracket = undefined, | |
.pc = 0, | |
.cycle_count = 0, | |
.program = program_bytes, | |
.max_cycles = max_cycles, | |
.input = input, | |
.input_index = 0, | |
.output = output, | |
.output_index = 0, | |
}; | |
// parse for matching brackets | |
for (program_bytes) |_, i| { | |
// default: no control flow modification | |
self.matching_bracket[i] = i; | |
} | |
var stack: [max_prg_bytes]usize = undefined; | |
var stack_index: usize = 0; | |
for (program_bytes) |byte, i| { | |
switch (byte) { | |
'[' => { | |
stack[stack_index] = i; | |
stack_index += 1; | |
}, | |
']' => if (stack_index != 0) { | |
stack_index -= 1; | |
const begin_index = stack[stack_index]; | |
self.matching_bracket[i] = begin_index; | |
self.matching_bracket[begin_index] = i; | |
}, | |
else => continue, | |
} | |
} | |
return self; | |
} | |
pub fn start(self: *BrainFuckInterpreter) void { | |
while (self.pc < self.program.len) { | |
if (self.cycle_count >= self.max_cycles) return; | |
switch (self.program[self.pc]) { | |
'<' => if (self.tape_head != 0) { | |
self.tape_head -= 1; | |
}, | |
'>' => if (self.tape_head < self.tape.len) { | |
self.tape_head += 1; | |
}, | |
'+' => self.tape[self.tape_head] +%= 1, | |
'-' => self.tape[self.tape_head] -%= 1, | |
'.' => if (self.output_index < self.output.len) { | |
self.output[self.output_index] = self.tape[self.tape_head]; | |
self.output_index += 1; | |
}, | |
',' => if (self.input_index >= self.input.len) { | |
self.tape[self.tape_head] = 0; | |
} else { | |
self.tape[self.tape_head] = self.input[self.input_index]; | |
self.input_index += 1; | |
}, | |
'[' => if (self.tape[self.tape_head] == 0) { | |
self.pc = self.matching_bracket[self.pc]; | |
}, | |
']' => if (self.matching_bracket[self.pc] != self.pc) { | |
self.pc = self.matching_bracket[self.pc]; | |
self.cycle_count += 1; | |
continue; | |
}, | |
else => {}, | |
} | |
self.pc += 1; | |
self.cycle_count += 1; | |
} | |
} | |
}; | |
test "interpreter" { | |
{ | |
const src = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>."; | |
var output: [100]u8 = undefined; | |
var bf = BrainFuckInterpreter.init(src, "", &output, 500); | |
bf.start(); | |
std.testing.expect(std.mem.eql(u8, bf.output[0..bf.output_index], "Hello World!\n")); | |
} | |
} | |
test "benchmark" { | |
var timer = try std.time.Timer.start(); | |
const start = timer.lap(); | |
var i: usize = 0; | |
var hash: usize = 0; | |
while (i < 1000000) : (i += 1) { | |
const src = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>."; | |
var output: [100]u8 = undefined; | |
var bf = BrainFuckInterpreter.init(src, "", &output, 500); | |
bf.start(); | |
for (bf.output[0..bf.output_index]) |byte| { | |
hash +%= byte; | |
} | |
} | |
const end = timer.lap(); | |
const elapsed_s = @intToFloat(f64, end - start) / std.time.ns_per_s; | |
std.debug.warn("time={} hash={}\n", .{ elapsed_s, hash }); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Zig is an ideal language for writing Redis modules, where software must have uncompromising performance and seamless interaction with C ABIs. In the talk we compare the development process of creating, testing, debugging, and measuring performance of a new Redis module in C vs Zig. | |
20 to 45 minute video presentation | |
0. show redismodule.h | |
1. hello world in C and Zig. | |
2. Show both of them working in Redis. | |
3. flip the vim tab back and forth to show it's the same structure | |
1. show the brainfuck C code and brainfuck Zig code, brief code tour | |
2. show that zig version is fewer lines of code | |
3. run the zig test suite | |
4. Show them both working on the redis server | |
5. introduce a segfault in the C version | |
6. introduce a the same problem in the zig version, show the stack trace | |
7. show how zig cc finds Undefined Behavior in the C code | |
1. copy paste the benchmarking code into bf.c | |
2. run the commands for benchmarking | |
- gcc | |
- clang | |
- zig cc | |
3. copy paste the benchmarking code into bf.zig | |
4. run the zig test command for benchmarking | |
5. show the results slide | |
6. give caveats |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#include "redismodule.h" | |
int HelloWorld_Command(RedisModuleCtx *ctx, RedisModuleString **argv, int argc) { | |
return RedisModule_ReplyWithSimpleString(ctx, "Hello World!"); | |
} | |
int RedisModule_OnLoad(RedisModuleCtx *ctx) { | |
if (RedisModule_Init(ctx, "hellomodule", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | |
return REDISMODULE_ERR; | |
return RedisModule_CreateCommand(ctx, "hello", HelloWorld_Command, "readonly", 0, 0, 0); | |
} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
usingnamespace @cImport(@cInclude("redismodule.h")); | |
export fn HelloWorld_Command(ctx: ?*RedisModuleCtx, argv: ?[*]?*RedisModuleString, argc: c_int) c_int { | |
return RedisModule_ReplyWithSimpleString.?(ctx, "Hello World!"); | |
} | |
export fn RedisModule_OnLoad(ctx: *RedisModuleCtx) c_int { | |
if (RedisModule_Init(ctx, "hellomodule", 1, REDISMODULE_APIVER_1) == REDISMODULE_ERR) | |
return REDISMODULE_ERR; | |
return RedisModule_CreateCommand.?(ctx, "hello", HelloWorld_Command, "readonly", 0, 0, 0); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
I have moved the
redismodules.h
into the folder in which I am compiling, not sure what am I doing wrong.