29K LoC zig vs 17K LoC C++ for equivalent functionality is surprising. Any more details about that?
Binary sizes is also surprising to me, are those comparisons stripped? Are they using -Os / -OReleaseSmall ?
I expect Zig to run circles around C++ on all these metrics, not only performance. Another one to look at would be peak RSS at runtime (how much memory does the app use).
Thanks for the interesting post and write-up on the experience.
You can find the actual code here for comparison:
- C++: factor/vm at 9d20144be8fb92bb49e8d514cdbe4ab2114a2384 · factor/factor · GitHub
- Zig: factor/src at 9d20144be8fb92bb49e8d514cdbe4ab2114a2384 · factor/factor · GitHub
It looks like a fairly faithful port. I haven’t looked too in-depth, but specifically regarding loc, a few things I can see which contribute to the increased count:
- Unit tests in zig vs. none in C++: factor/src/jit.zig at 9d20144be8fb92bb49e8d514cdbe4ab2114a2384 · factor/factor · GitHub
- Some more explicit listing of definitions compared to x-macro c++ variants: factor/src/primitives.zig at 9d20144be8fb92bb49e8d514cdbe4ab2114a2384 · factor/factor · GitHub vs. factor/vm/primitives.hpp at 9d20144be8fb92bb49e8d514cdbe4ab2114a2384 · factor/factor · GitHub (you could definitely compact this and generate these table listings at comptime though).
- Couple of changes for error-handling, specifically the zig version has added error handling in place of global handling, is also a bit more verbose on some of the c errno handling (for good reason): factor/src/io.zig at 9d20144be8fb92bb49e8d514cdbe4ab2114a2384 · factor/factor · GitHub vs factor/vm/io.cpp at 9d20144be8fb92bb49e8d514cdbe4ab2114a2384 · factor/factor · GitHub
Probable candidate for the binary size issue:
andy@bark ~/s/factor (master)> g 'inline fn'
src/gc.zig:pub inline fn objectOrFreeSize(addr: Cell) Cell {
src/free_list.zig: inline fn sizeToIndex(size: Cell) ?usize {
src/free_list.zig:pub inline fn objectSizeFromHeader(address: Cell) Cell {
src/callstack_lookup.zig: pub inline fn ownerForAddress(self: *Self, address: Cell) ?*const code_blocks.CodeBlock {
src/callstack_lookup.zig: pub inline fn ownerForAddressUnsafe(self: *Self, address: Cell) ?*const code_blocks.CodeBlock {
src/callstack_lookup.zig: pub inline fn callsiteIndex(self: *Self, gc_info: *const code_blocks.GcInfo, return_address_offset: u32) ?u32 {
src/callstack_lookup.zig: pub inline fn frameSizeFromAddress(owner: *const code_blocks.CodeBlock, addr: Cell) Cell {
src/code_heap.zig:inline fn isNonDecreasing(values: []const Cell) bool {
src/data_heap.zig: pub inline fn contains(self: *const Self, addr: Cell) bool {
src/data_heap.zig: pub inline fn contains(self: *const Self, addr: Cell) bool {
src/data_heap.zig: pub inline fn contains(self: *const Self, addr: Cell) bool {
src/mark_bits.zig: pub inline fn addressToBitIndex(self: *const Self, address: Cell) ?struct { cell_index: usize, bit_index: u6 } {
src/mark_bits.zig: pub inline fn setMarked(self: *Self, address: Cell, data_size: Cell) void {
src/mark_bits.zig: inline fn setMarkedFromLine(self: *Self, address: Cell, data_size: Cell, start_line: Cell) void {
src/mark_bits.zig: pub inline fn tryMarkStart(self: *Self, address: Cell, data_size: Cell) bool {
src/mark_bits.zig: pub inline fn tryMarkStartBitOnly(self: *Self, address: Cell) bool {
src/mark_bits.zig: pub inline fn isMarked(self: *const Self, address: Cell) bool {
src/mark_bits.zig: pub inline fn forwardBlock(self: *const Self, original: Cell) Cell {
src/mark_bits.zig: inline fn lineBlock(self: *const Self, line: Cell) Cell {
src/mark_bits.zig: pub inline fn nextMarkedBlockAfter(self: *const Self, original: Cell) Cell {
src/mark_bits.zig: pub inline fn nextUnmarkedBlockAfter(self: *const Self, original: Cell) Cell {
src/mark_bits.zig: pub inline fn unmarkedBlockSize(self: *const Self, original: Cell) Cell {
src/spill_slots.zig:pub inline fn visit(
src/bump_allocator.zig: pub inline fn allocate(self: *BumpAllocator, size: Cell) Cell {
src/write_barrier.zig:pub inline fn addrToCard(addr: Cell) Cell {
src/write_barrier.zig:pub inline fn addrToDeck(addr: Cell) Cell {
src/write_barrier.zig: inline fn blockIndex(self: *const Self, block: *CodeBlock) usize {
src/write_barrier.zig: inline fn refreshHasAny(self: *Self) void {
src/primitives/code.zig:pub inline fn objectClass(object: Cell) Cell {
src/primitives/code.zig:inline fn methodCacheHashcode(klass: Cell, cache_arr: *const layouts.Array) Cell {
src/primitives/code.zig:inline fn updateMethodCache(vm: *FactorVM, cache: Cell, klass: Cell, method: Cell) void {
src/primitives/code.zig:inline fn searchLookupAlist(table: Cell, klass: Cell) Cell {
src/primitives/code.zig:inline fn searchLookupHash(table: Cell, klass: Cell, hashcode: Cell) Cell {
src/primitives/code.zig:inline fn lookupTupleMethod(obj: Cell, methods: Cell) Cell {
src/primitives/code.zig:pub inline fn lookupMethod(object: Cell, methods: Cell) Cell {
src/primitives/math.zig:noinline fn binaryBignumSlow(
src/primitives/math.zig:noinline fn binaryBignumCmpSlow(vm: *FactorVM, a_cell: Cell, b_cell: Cell) bignum.Comparison {
src/vm.zig: pub inline fn getVM(self: *VMAssemblyFields) *FactorVM {
src/vm.zig: pub inline fn writeBarrierKnownHeap(self: *Self, slot_ptr: *Cell) void {
src/vm.zig: pub inline fn writeBarrierKnownHeapWithValue(self: *Self, slot_ptr: *Cell, value: Cell) void {
src/vm.zig: pub inline fn writeBarrier(self: *Self, slot_ptr: *Cell) void {
src/vm.zig: pub inline fn writeBarrierWithValue(self: *Self, slot_ptr: *Cell, value: Cell) void {
src/vm.zig: pub inline fn getCtx(self: *Self) *contexts.Context {
src/vm.zig: pub inline fn setCtx(self: *Self, c: *contexts.Context) void {
src/vm.zig: pub inline fn getNursery(self: *Self) *bump_allocator.BumpAllocator {
src/vm.zig: pub inline fn getSpecialObjects(self: *Self) *[objects.special_object_count]Cell {
src/vm.zig: pub inline fn peek(self: *const Self) Cell {
src/vm.zig: pub inline fn pop(self: *Self) Cell {
src/vm.zig: pub inline fn push(self: *Self, value: Cell) void {
src/vm.zig: pub inline fn replace(self: *Self, value: Cell) void {
src/vm.zig: pub inline fn allotUninitializedArrayNoFill(self: *Self, capacity: Cell) ?Cell {
src/vm.zig: pub inline fn allotUninitializedArray(self: *Self, capacity: Cell) ?Cell {
src/vm.zig: pub inline fn allotObject(self: *Self, type_tag: layouts.TypeTag, size: Cell) ?Cell {
src/object_start_map.zig: inline fn addressToCard(self: *const Self, address: Cell) usize {
src/object_start_map.zig: inline fn cardToAddress(self: *const Self, card_index: usize) Cell {
src/object_start_map.zig: pub inline fn recordObjectStart(self: *Self, address: Cell) void {
src/object_start_map.zig: inline fn addressToCard(self: *const Self, address: Cell) usize {
src/object_start_map.zig: inline fn addressToDeck(self: *const Self, address: Cell) usize {
src/primitives.zig:pub inline fn callPrimitive(vm: *FactorVM, index: u16) void {
src/contexts.zig: pub inline fn isActive(self: *const Self) bool {
src/contexts.zig: pub inline fn peek(self: *const Self) Cell {
src/contexts.zig: pub inline fn replace(self: *Self, tagged: Cell) void {
src/contexts.zig: pub inline fn pop(self: *Self) Cell {
src/contexts.zig: pub inline fn push(self: *Self, tagged: Cell) void {
src/contexts.zig: pub inline fn peekRetain(self: *const Self) Cell {
src/contexts.zig: pub inline fn popRetain(self: *Self) Cell {
src/contexts.zig: pub inline fn pushRetain(self: *Self, tagged: Cell) void {
src/fixnum.zig:pub inline fn toBignum(vm: *FactorVM, n: Fixnum) !*layouts.Bignum {
src/slot_visitor.zig:pub inline fn visitObjectSlots(address: Cell, callback: SlotVisitorFn, ctx: *anyopaque) void {
src/slot_visitor.zig: pub inline fn allocate(self: *CopyingDestination, size: Cell) ?Cell {
src/slot_visitor.zig: inline fn inSourceGeneration(self: *const CopyingDestination, addr: Cell) bool {
src/slot_visitor.zig:inline fn copySlot(slot: *Cell, destination: *CopyingDestination) void {
src/layouts.zig:pub inline fn TAG(x: Cell) Cell {
src/layouts.zig:pub inline fn UNTAG(x: Cell) Cell {
src/layouts.zig:pub inline fn RETAG(x: Cell, new_tag: Cell) Cell {
src/layouts.zig:pub inline fn hasTag(x: Cell, type_tag: TypeTag) bool {
src/layouts.zig:pub inline fn typeTag(x: Cell) TypeTag {
src/layouts.zig:pub inline fn typeHasNoPointers(type_tag: TypeTag) bool {
src/layouts.zig:pub inline fn isImmediate(obj: Cell) bool {
src/layouts.zig:pub inline fn untagFixnum(tagged: Cell) Fixnum {
src/layouts.zig:pub inline fn untagFixnumUnsigned(tagged: Cell) Cell {
src/layouts.zig:pub inline fn untagFixnumFast(tagged: Cell) Cell {
src/layouts.zig:pub inline fn tagFixnum(untagged: Fixnum) Cell {
src/layouts.zig:pub inline fn alignCell(a: Cell, b: Cell) Cell {
src/layouts.zig:pub inline fn alignmentFor(a: Cell, b: Cell) Cell {
src/layouts.zig: pub inline fn isFree(self: *const Object) bool {
src/layouts.zig: pub inline fn getType(self: *const Object) TypeTag {
src/layouts.zig: pub inline fn isForwardingPointer(self: *const Object) bool {
src/layouts.zig: pub inline fn forwardingPointer(self: *const Object) *Object {
src/layouts.zig: pub inline fn forwardTo(self: *Object, pointer: *Object) void {
src/layouts.zig:pub inline fn followForwardingPointers(addr: Cell) Cell {
src/bignum.zig:pub inline fn divmod128by64(hi: u64, lo: u64, divisor: u64) struct { q: u64, r: u64 } {
src/bignum.zig: pub inline fn rawData(self: *const Self) [*]Cell {
src/bignum.zig: pub inline fn length(self: *const Bignum) Cell {
src/bignum.zig: pub inline fn rawCapacity(self: *const Bignum) Cell {
src/bignum.zig: pub inline fn isNegative(self: *const Bignum) bool {
src/bignum.zig: pub inline fn isZero(self: *const Bignum) bool {
src/bignum.zig: pub inline fn digits(self: *const Bignum) [*]Cell {
src/bignum.zig: pub inline fn getDigit(self: *const Bignum, index: Cell) Cell {
src/bignum.zig: pub inline fn setDigit(self: *Bignum, index: Cell, value: Cell) void {
src/bignum.zig:pub inline fn compareUnsigned(x: *const Bignum, y: *const Bignum) Comparison {
src/bignum.zig:pub inline fn integerLength(x: *const Bignum) Cell {
src/bignum.zig:inline fn cachedBignum(vm: *FactorVM, which: objects.SpecialObject) ?*Bignum {
src/bignum.zig:inline fn zeroBignum(vm: *FactorVM) !*Bignum {
src/bignum.zig:pub inline fn toFixnum(bn: *const Bignum) Fixnum {
src/bignum.zig:pub inline fn fitsFixnum(bn: *const Bignum) bool {
src/bignum.zig:pub inline fn maybeToFixnum(bn: *const Bignum) Cell {
src/bignum.zig:pub inline fn allocBignum(vm: *FactorVM, len: Cell, negative: bool) !*Bignum {
src/bignum.zig:inline fn bignumPosPosOp(vm: *FactorVM, x_in: *const Bignum, y_in: *const Bignum, comptime op: BitwiseOp) !*Bignum {
src/bignum.zig:inline fn setUnsignedLength(bn: *Bignum, len: Cell) void {
src/bignum.zig:inline fn shiftLeft(vm: *FactorVM, x_in: *const Bignum, shift_bits: Cell) !*Bignum {
src/bignum.zig:inline fn shiftRight(vm: *FactorVM, x_in: *const Bignum, shift_bits: Cell) !*Bignum {
src/bignum.zig:inline fn trim(vm: *FactorVM, bn: *Bignum) !*Bignum {
src/mark.zig:inline fn markDataIfNeeded(gc: *GC, addr: Cell, size: Cell) void {
src/mark.zig:inline fn markStackPush(gc: *GC, value: Cell) void {
src/mark.zig:inline fn fullMarkValue(ctx: *FullMarkContext, value: Cell) Cell {
src/mark.zig:inline fn fullMarkStackSlots(top: Cell, seg: *const segments.Segment, ctx: *FullMarkContext) void {
src/mark.zig:inline fn fullMarkSlotInline(slot: *Cell, ctx: *FullMarkContext) void {
src/mark.zig:inline fn fullMarkEntryPoint(gc: *GC, entry_point: Cell) void {
src/mark.zig:inline fn markStackSlots(top: Cell, seg: *const segments.Segment, gc: *GC, tenured: *data_heap_mod.TenuredSpace) void {
src/card_scan.zig:inline fn slotCountAndSize(obj_addr: Cell) SlotCountAndSize {
src/card_scan.zig:inline fn visitSlot(slot: *Cell, destination: *slot_visitor.CopyingDestination) void {
src/compact.zig:inline fn fixupStackSlots(top: Cell, seg: *const segments.Segment, fixup: *CompactionFixup) void {
src/inline_cache.zig: pub inline fn getCallTargetUnchecked(return_address: Cell) Cell {
src/code_blocks.zig: pub inline fn isFree(self: *const Self) bool {
src/code_blocks.zig: pub inline fn size(self: *const Self) Cell {
src/code_blocks.zig: pub inline fn entryPoint(self: *const Self) Cell {
src/code_blocks.zig: pub inline fn loadCodeBlock(self: *const Self) ?*CodeBlock {
src/code_blocks.zig:inline fn computeEntryPoint(obj: Cell) Cell {
andy@bark ~/s/factor (master)>
Quoting Documentation - The Zig Programming Language
It is generally better to let the compiler decide when to inline a function, except for these scenarios:
- To change how many stack frames are in the call stack, for debugging purposes.
- To force comptime-ness of the arguments to propagate to the return value of the function, as in the above example.
- Real world performance measurements demand it.
Note thatinlineactually restricts what the compiler is allowed to do. This can harm binary size, compilation speed, and even runtime performance.