Getting Ziggy With It – Re: Factor

8 Likes

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

4 Likes

Thanks for the interesting post and write-up on the experience.

You can find the actual code here for comparison:

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:

3 Likes

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 that inline actually restricts what the compiler is allowed to do. This can harm binary size, compilation speed, and even runtime performance.
7 Likes