diff --git a/CMakeLists.txt b/CMakeLists.txt index f67a53f..2812474 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,13 +1,13 @@ cmake_minimum_required(VERSION 3.25) project(pxkorka VERSION 0.1.0 - LANGUAGES CXX + LANGUAGES C CXX ) set(CMAKE_CXX_STANDARD 26) set(CMAKE_CXX_STANDARD_REQUIRED ON) -set(CMAKE_CXX_EXTENSIONS OFF) +set(CMAKE_CXX_EXTENSIONS ON) # --- OPTIONS --- option(ENABLE_TESTS "Build tests" ON) @@ -16,7 +16,7 @@ option(ENABLE_TESTS "Build tests" ON) if (MSVC) add_compile_options(/W4 /WX /permissive-) else () - add_compile_options(-Wall -Wextra -Wpedantic -Werror -Wshadow -Wunused) +# add_compile_options(-Wall -Wextra -Wpedantic -Werror -Wshadow -Wunused) endif () # Dependencies @@ -88,4 +88,34 @@ target_compile_options(korka_lib PUBLIC -Wno-error=unused-but-set-variable -Wno- # ) # # catch_discover_tests(pxkorka_tests) -#endif () \ No newline at end of file +#endif () + + +# --- BENCH --- +CPMAddPackage( + NAME lua + GIT_REPOSITORY https://github.com/lua/lua.git + VERSION 5.3.5 + DOWNLOAD_ONLY YES +) + +if (lua_ADDED) + # lua has no CMake support, so we create our own target + + FILE(GLOB lua_sources ${lua_SOURCE_DIR}/*.c) + list(REMOVE_ITEM lua_sources "${lua_SOURCE_DIR}/lua.c" "${lua_SOURCE_DIR}/luac.c") + add_library(lua STATIC ${lua_sources}) + + target_include_directories(lua + PUBLIC + $ + ) +endif() + + +find_package(Python3 COMPONENTS Development REQUIRED) + +add_executable(bench bench.cpp) +target_link_libraries(bench PUBLIC lua korka_lib) +target_link_libraries(bench PRIVATE Python3::Python) +target_include_directories(bench PRIVATE ${Python3_INCLUDE_DIRS}) \ No newline at end of file diff --git a/README.md b/README.md index d22d3b9..010b2ba 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -KorkaVM icon +Korka icon -# KorkaVM +# Korka A Virtual Machine where lexing and compilation happen entirely at **compile-time**. @@ -8,22 +8,27 @@ A Virtual Machine where lexing and compilation happen entirely at **compile-time ### What is this -KorkaVM is a project where I'm trying to create a tool that allows to embed logic +Korka is a project where I'm trying to create a tool that allows to embed logic without runtime overhead of parsing or loading external files. You write C-like code right inside C++, and the compiler transforms it into internal bytecode before your program even starts. ### Status -| Component | Stage | Execution context | -|:----------------:|:-------:|:-----------------:| -| Lexer | Done | constexpr | -| Bytecode builder | Done | constexpr | -| Parser | Done | constexpr | -| Compiler | Partially done | constexpr | -| VM runner | Partially done | runtime | +| Component | Stage | Execution context | +|:----------------:|:-----:|:-----------------:| +| Lexer | Done | constexpr | +| Bytecode builder | Done | constexpr | +| Parser | Done | constexpr | +| Compiler | Done | constexpr | +| VM runner | Done | runtime | + +### Features + ++ **Full interop** between Korka & C++, you can bind a C++ function into the script and vice versa. + Type safety guaranteed. ++ **Fast VM**: in my `fib` benchmark Korka surpasses Lua and Python by 30-40% -What's done: ```cpp constexpr char code[] = R"( int main() { @@ -40,7 +45,12 @@ int foo(int a, int b) { } )"; -constexpr auto compile_result = korka::compile(); + +constexpr auto bindings = korka::make_bindings( + korka::wrap("cpp_fib"), + korka::wrap("print_n") +); +constexpr auto compile_result = korka::compile(); // Extracting function types from code // It returns a pointer, bc you can't return a type ._. @@ -51,39 +61,116 @@ auto foo_func = compile_result.function<"foo">(); static_assert(std::is_same_v); ``` - -## Example context +### Code example ```cpp +// Functions that are interoped into Korka +auto fib(std::int64_t n) -> std::int64_t { + if (n == 0) return 0; + if (n == 1) return 1; + + return fib(n - 1) + fib(n - 2); +} -auto foo(int a) -> int { - return a * 2 + 5; +auto print_n(std::int64_t n) -> void { + std::cout << n << '\n'; } +// C-like code +constexpr char code[] = R"( +int fib(int n) { + if (n == 0) return 0; + if (n == 1) return 1; + + return fib(n-1) + cpp_fib(n-2); +} + +void print_fib(int n) { + int result = fib(n); + + print_n(result); + return; +} +)"; + constexpr auto bindings = korka::make_bindings( - "foo", &foo + korka::wrap("cpp_fib"), + korka::wrap("print_n") ); -constexpr auto my_script = korka::compile(bindings, R"( - int calculate(int x) { - if (x <= 0) return 0; +constexpr auto compile_result = korka::compile(); - return foo(x) / 3; - } -)"); +constexpr auto script_fib = compile_result.function<"fib">(); +constexpr auto script_print_fib = compile_result.function<"print_fib">(); -// Simple usage int main() { - korka::runtime vm; - int result = vm.execute(my_script) + korka::vm::context ctx{ + compile_result.bytes, bindings + }; + + // result will have int64_t type + auto result = ctx.call(script_fib, 12L); + std::cout << result << '\n'; + + ctx.call(script_print_fib, 16L); } +``` -// Not so simple usage -int main() { - korka::runtime vm; - - // Byte code gets inserted right into the native instruction flow - // and executed by vm right there - korka::run_embed(vm); +## Benchmark + +See the [bench.cpp](https://github.com/PyXiion/pxkorka/blob/master/bench.cpp) for full code. + + +
+ +Code + +--- + +### Python code + +```python +def fib(n): + if n == 0: return 0 + if n == 1: return 1 + return fib(n - 1) + fib(n - 2) +``` + +### Lua code + +```lua +function fib(n) + if n == 0 then return 0 end + if n == 1 then return 1 end + return fib(n-1) + fib(n-2) +end +``` + +### C code (Korka) + +```c +int fib(int n) { + if (n==0) return 0; + if (n==1) return 1; + return fib(n-1) + fib(n-2); } ``` + +--- + +
+ +### Results: + +| n | Iterations | Korka (ms) | Lua (ms) | Python (ms) | Korka Speedup (vs. Python) | +|---:|------------:|-----------:|----------:|------------:|:--------------------------:| +| 10 | 2,000,000 | 6,503.03 | 9,686.56 | 8,988.40 | 1.38x | +| 15 | 200,000 | 7,113.14 | 10,899.20 | 9,996.17 | 1.41x | +| 20 | 20,000 | 8,546.09 | 11,765.60 | 10,961.60 | 1.28x | +| 23 | 4,500 | 7,964.48 | 11,491.50 | 10,667.10 | 1.34x | +| 25 | 2,000 | 8,900.08 | 12,980.40 | 12,277.30 | 1.38x | +| 28 | 400 | 7,506.88 | 11,101.50 | 10,820.10 | 1.44x | +| 30 | 200 | 9,910.64 | 14,334.40 | 13,697.50 | 1.38x | + +Cool graph here: +![Execution time graph](img.png) \ No newline at end of file diff --git a/bench.cpp b/bench.cpp new file mode 100644 index 0000000..ba71a4c --- /dev/null +++ b/bench.cpp @@ -0,0 +1,175 @@ +#include +#include +#include +#include +#include +#include +#include + +#include "korka/compiler/compiler.hpp" +#include "korka/compiler/binding.hpp" +#include "korka/vm/vm_runtime.hpp" + +[[gnu::noinline]] +auto native_fib(std::int64_t n) -> std::int64_t { + if (n == 0) return 0; + if (n == 1) return 1; + + return native_fib(n - 1) + native_fib(n - 2); +} + +const char *py_code = R"( +def fib(n): + if n == 0: return 0 + if n == 1: return 1 + return fib(n-1) + fib(n-2) +)"; + +const char *lua_code = R"( +function fib(n) + if n == 0 then return 0 end + if n == 1 then return 1 end + return fib(n-1) + fib(n-2) +end +)"; + +constexpr char korka_code[] = "int fib(int n) { if (n==0) return 0; if (n==1) return 1; return fib(n-1)+fib(n-2); }"; + +struct Result { + int n; + int iterations; + double korka_ms; + double lua_ms; + double python_ms; + double cpp_ms; +}; + +void run_benchmarks() { + // clang-format off + // @formatter:off + const std::vector> test_cases = { + {10, 2'000'000}, + {15, 200'000}, + {20, 20'000}, + {23, 4'500}, + {25, 2'000}, + {28, 400}, + {30, 200} + }; + // @formatter:on + // clang-format on + + std::vector results; + + std::cout << "=== Korka vs Lua vs Python vs C++ ===\n\n"; + + std::cout << "=== Phase 1: Initialization ===\n"; + + auto py_start = std::chrono::high_resolution_clock::now(); + Py_Initialize(); + PyObject *py_main = PyImport_AddModule("__main__"); + PyObject *py_dict = PyModule_GetDict(py_main); + PyRun_String(py_code, Py_file_input, py_dict, py_dict); + PyObject *py_fib_func = PyDict_GetItemString(py_dict, "fib"); + auto py_end = std::chrono::high_resolution_clock::now(); + + auto lua_start = std::chrono::high_resolution_clock::now(); + lua_State *L = luaL_newstate(); + luaL_openlibs(L); + luaL_dostring(L, lua_code); + auto lua_end = std::chrono::high_resolution_clock::now(); + + auto korka_start = std::chrono::high_resolution_clock::now(); + constexpr static auto bindings = korka::make_bindings(); + constexpr auto korka_bytecode = korka::compile(); + constexpr auto korka_fib_addr = korka_bytecode.function<"fib">(); + korka::vm::context ctx{korka_bytecode.bytes, bindings}; + auto korka_end = std::chrono::high_resolution_clock::now(); + + auto t_py = std::chrono::duration(py_end - py_start).count(); + auto t_lua = std::chrono::duration(lua_end - lua_start).count(); + auto t_korka = std::chrono::duration(korka_end - korka_start).count(); + + printf("Python Init: %8.1f µs\n", t_py); + printf("Lua Init: %8.1f µs\n", t_lua); + printf("Korka Init: %8.1f µs\n\n", t_korka); + + std::cout << "=== Phase 2: Execution (different fib depths) ===\n\n"; + std::cout << std::setw(6) << "n" + << std::setw(12) << "iters" + << std::setw(12) << "Korka ms" + << std::setw(12) << "Lua ms" + << std::setw(12) << "Python ms" + << std::setw(12) << "C++ ms" + << std::setw(14) << "Speedup" << "\n"; + std::cout << std::string(72, '-') << "\n"; + + for (const auto &[n, iterations]: test_cases) { + Result r{.n = n, .iterations = iterations}; + + // Korka + auto k_start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iterations; ++i) { + volatile auto res = ctx.call(korka_fib_addr, static_cast(n)); + } + auto k_end = std::chrono::high_resolution_clock::now(); + r.korka_ms = std::chrono::duration(k_end - k_start).count(); + + // Lua + auto l_start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iterations; ++i) { + lua_getglobal(L, "fib"); + lua_pushinteger(L, n); + lua_pcall(L, 1, 1, 0); + lua_pop(L, 1); + } + auto l_end = std::chrono::high_resolution_clock::now(); + r.lua_ms = std::chrono::duration(l_end - l_start).count(); + + // Python + auto p_start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iterations; ++i) { + PyObject *args = PyTuple_Pack(1, PyLong_FromLong(n)); + PyObject *result = PyObject_CallObject(py_fib_func, args); + Py_DECREF(args); + Py_DECREF(result); + } + auto p_end = std::chrono::high_resolution_clock::now(); + r.python_ms = std::chrono::duration(p_end - p_start).count(); + + // C++ + auto cpp_start = std::chrono::high_resolution_clock::now(); + for (int i = 0; i < iterations; ++i) { + volatile auto a = native_fib(n); + (void) a; + } + auto cpp_end = std::chrono::high_resolution_clock::now(); + r.cpp_ms = std::chrono::duration(cpp_end - cpp_start).count(); + + results.push_back(r); + + double speedup = r.python_ms / r.korka_ms; + + printf("%6d %11d %11.2f %11.2f %11.2f %11.2f %12.2fx\n", + n, iterations, r.korka_ms, r.lua_ms, r.python_ms, r.cpp_ms, speedup); + } + + std::ofstream csv("fib_benchmark.csv"); + csv << "n,iterations,korka_ms,lua_ms,python_ms,korka_speedup_vs_python\n"; + for (const auto &r: results) { + double speedup = r.python_ms / r.korka_ms; + csv << r.n << "," << r.iterations << "," + << r.korka_ms << "," << r.lua_ms << "," << r.python_ms << "," + << speedup << "\n"; + } + csv.close(); + + std::cout << "=== DONE ==="; + Py_Finalize(); + lua_close(L); +} + +int main() { + run_benchmarks(); + return 0; +} \ No newline at end of file diff --git a/include/korka/compiler/binding.hpp b/include/korka/compiler/binding.hpp index 972feb2..80c9a80 100644 --- a/include/korka/compiler/binding.hpp +++ b/include/korka/compiler/binding.hpp @@ -101,10 +101,6 @@ namespace korka { } }; - class const_bindings { - - }; - consteval auto make_bindings(auto ...wrapped_functions) { constexpr auto func_max_args = [] { std::size_t max{}; diff --git a/include/korka/utils/byte_reader.hpp b/include/korka/utils/byte_reader.hpp index 378b97a..2ebdf3a 100644 --- a/include/korka/utils/byte_reader.hpp +++ b/include/korka/utils/byte_reader.hpp @@ -43,6 +43,10 @@ namespace korka::vm { m_cursor = pos; } + auto data() noexcept -> const std::byte * { + return m_bytes.data(); + } + private: std::size_t m_cursor; const std::span m_bytes; diff --git a/include/korka/vm/op_codes.hpp b/include/korka/vm/op_codes.hpp index 868183c..94c4536 100644 --- a/include/korka/vm/op_codes.hpp +++ b/include/korka/vm/op_codes.hpp @@ -19,10 +19,6 @@ namespace korka::vm { // lload, - // Load parameters from stack into locals - // - pload, - // Pops a value from stack and saves to the local at index // lsave, diff --git a/include/korka/vm/vm_runtime.hpp b/include/korka/vm/vm_runtime.hpp index 6b13713..9107d9b 100644 --- a/include/korka/vm/vm_runtime.hpp +++ b/include/korka/vm/vm_runtime.hpp @@ -33,7 +33,6 @@ namespace korka::vm { // Create a scope for the call push_scope(); - // TODO: check argument types // Pass arguments std::size_t i = 0; @@ -43,16 +42,7 @@ namespace korka::vm { m_reader.set_cursor(func.start_pos); - // Run until ret - // Check scopes size to ensure it's the original function's return - while (true) { - auto op = execute_op(); - - if (op == op_code::ret && m_scopes.empty()) { - m_scopes.clear(); - break; - } - } + execute_loop(); using return_type = typename Traits::return_type; if constexpr (std::is_void_v) { @@ -65,114 +55,158 @@ namespace korka::vm { protected: bindings_t m_bindings; - auto execute_op() -> op_code { - auto initial_pc = m_reader.cursor(); + void execute_loop() { + const std::byte *ip = m_reader.data() + m_reader.cursor(); + + // Using macros instead of lambdas, because + // templates with lambdas (like lambda) don't work + // well +#define READ(T) (*reinterpret_cast(ip)); ip += sizeof(T) +#define IP_POS() static_cast(ip - m_reader.data()) + + // Using dispatch table instead of switch case, because it's + // much faster for the CPU + // Also instead of calling execute_op every time I do it right here + // without extra calls + + // Also using C99 extensions, I should get rid of them later. + static const void *const dispatch[] = { + [int(op_code::lload)] = &&op_lload, + [int(op_code::lsave)] = &&op_lsave, + [int(op_code::i64_const)] = &&op_i64_const, + [int(op_code::i64_add)] = &&op_i64_add, + [int(op_code::i64_sub)] = &&op_i64_sub, + [int(op_code::i64_mul)] = &&op_i64_mul, + [int(op_code::i64_div)] = &&op_i64_div, + [int(op_code::i64_cmp)] = &&op_i64_cmp, + [int(op_code::jmp)] = &&op_jmp, + [int(op_code::jmpz)] = &&op_jmpz, + [int(op_code::call)] = &&op_call, + [int(op_code::ret)] = &&op_ret, + [int(op_code::trap)] = &&op_trap, + }; + + op_start: + const std::size_t instr_start = IP_POS(); + const op_code code = READ(op_code); + + // @formatter:off + // clang-format off + goto *dispatch[int(code)]; + // clang-format on + // @formatter:on + + op_lload: + { + const auto index = READ(local_index_t); + push_value(current_scope()->get_local_value(index)); + goto op_start; + } - const auto code = m_reader.read(); + op_lsave: + { + const auto index = READ(local_index_t); + current_scope()->set_local_value(index, pop_value()); + goto op_start; + } - switch (code) { - case op_code::lload: { - const auto index = m_reader.read(); - push_value(current_scope()->get_local_value(index)); - } - break; - case op_code::pload: - throw std::runtime_error("Not implemented"); - break; - case op_code::lsave: { - const auto index = m_reader.read(); - current_scope()->set_local_value(index, pop_value()); - } - break; - case op_code::i64_const: { - const auto v = m_reader.read(); - push(v); - } - break; - case op_code::i64_add: { - const auto b = pop(); - const auto a = pop(); - push(a + b); - } - break; - case op_code::i64_sub: { - const auto b = pop(); - const auto a = pop(); - push(a - b); - } - break; - case op_code::i64_mul: { - const auto b = pop(); - const auto a = pop(); - push(a * b); - } - break; - case op_code::i64_div: { - const auto b = pop(); - const auto a = pop(); - push(a / b); - } - break; - case op_code::i64_cmp: { - const auto b = pop(); - const auto a = pop(); - push(static_cast(a == b)); - } - break; - case op_code::jmp: { - const auto offset = m_reader.read(); - m_reader.set_cursor(initial_pc + offset); - } - break; - case op_code::jmpz: { - const auto offset = m_reader.read(); - auto v = pop(); - if (v == 0) { - m_reader.set_cursor(initial_pc + offset); - } - } - break; - case op_code::call: { - auto called_address = m_reader.read(); - current_scope()->suspension_point = m_reader.cursor(); - auto arg_count = pop(); + op_i64_const: + { + const auto v = READ(std::int64_t); + push(v); + goto op_start; + } + + op_i64_add: + { + const auto b = pop(); + const auto a = pop(); + push(a + b); + goto op_start; + } + op_i64_sub: + { + const auto b = pop(); + const auto a = pop(); + push(a - b); + goto op_start; + } + op_i64_mul: + { + const auto b = pop(); + const auto a = pop(); + push(a * b); + goto op_start; + } + op_i64_div: + { + const auto b = pop(); + const auto a = pop(); + push(a / b); + goto op_start; + } + op_i64_cmp: + { + const auto b = pop(); + const auto a = pop(); + push(static_cast(a == b)); + goto op_start; + } - push_scope(); + op_jmp: + { + const auto offset = READ(jump_offset); + ip = m_reader.data() + (instr_start + offset); + goto op_start; + } + + op_jmpz: + { + const auto offset = READ(jump_offset); + const auto v = pop(); + if (__builtin_expect(v == 0, 0)) { + ip = m_reader.data() + (instr_start + offset); + } + goto op_start; + } - // Load args - for (int i = arg_count - 1; i >= 0; --i) { - auto v = pop_value(); - current_scope()->set_local_value(i, v); - } + op_call: + { + auto called_address = READ(address_t); + current_scope()->suspension_point = IP_POS(); + auto arg_count = pop(); - m_reader.set_cursor(called_address); + push_scope(); + for (int i = arg_count - 1; i >= 0; --i) { + current_scope()->set_local_value(i, pop_value()); } - break; - case op_code::ret: - pop_scope(); - if (current_scope()) { - m_reader.set_cursor(current_scope()->suspension_point); - } - break; - case op_code::trap: { - auto id = m_reader.read(); - auto func = m_bindings.get_callable_by_id(id); - (*func)(*this); + ip = m_reader.data() + called_address; + goto op_start; + } + + op_ret: + { + pop_scope(); + if (current_scope()) { + ip = m_reader.data() + current_scope()->suspension_point; + } else { + m_scopes.clear(); + m_reader.set_cursor(IP_POS()); + return; } - break; + goto op_start; } - return code; + op_trap: + { + auto id = READ(vm_external_function_id); + auto func = m_bindings.get_callable_by_id(id); + (*func)(*this); + goto op_start; + } } - }; - - class runtime { - public: -// auto create_context() -> context { -// return {}; -// } - +#undef READ +#undef IP_POS }; - } // korka::vm \ No newline at end of file diff --git a/main.cpp b/main.cpp index ed16b81..37ebd43 100644 --- a/main.cpp +++ b/main.cpp @@ -50,33 +50,4 @@ int main() { std::cout << result << '\n'; ctx.call(script_print_fib, 16L); - -// std::println("{}", ctx.call(fib_func, 12L)); - -// std::ignore = tokens; -// std::println("{:n:02X}", compile_result.bytes | std::views::transform([](auto b) { return static_cast(b); })); - -// auto lexed = korka::lexer{code}.lex(); -// if (not lexed) { -// std::println("{}", korka::to_string(lexed.error())); -// return 0; -// } -// -// auto parsed = korka::parser{lexed.value()}.parse(); -// if (not parsed) { -// std::println("{}", korka::to_string(parsed.error())); -// return 0; -// } -// auto [node_pool, node_root] = parsed.value(); -// std::println("{}", korka::ast_walker{node_pool, node_root, 0}); -// -// korka::compiler compiler{node_pool, node_root}; -// auto bytes = compiler.compile(); -// -// if (bytes) { -// std::println("{::X}", *bytes | std::views::transform([](auto b) { return static_cast(b); })); -// } else { -// std::println("{}", korka::to_string(bytes.error())); -// } - } \ No newline at end of file diff --git a/media/execution_time_graph.png b/media/execution_time_graph.png new file mode 100644 index 0000000..3c24161 Binary files /dev/null and b/media/execution_time_graph.png differ diff --git a/icon.svg b/media/icon.svg similarity index 100% rename from icon.svg rename to media/icon.svg