Live data from Hacker News

Go is my hammer, and everything is a nail

maragu.dev

561–570 of 816 posts

Re: Go is my hammer, and everything is a nail

#561

Earlier quoted context omitted.

Can't Go compiler statically prove that such single implementation interfaces are indeed that and devirtualize the callsites referring to them? Either way, the problem seems to happen in most languages of today, if they (or their community) ever happen to accidentally encourage passing an opaque type abstraction over a concrete one.

I think it actually does that, but in local contexts, where this analysis is somewhat easy. I also believe you don't actually have to prove it statically: PGO can collect enough data to e.g. add a check that a certain type is usually X, and follow a slow path otherwise

I understand that it does so when the exact type is observed - a direct call on a concrete type. But I was wondering if it performs whole-program-view optimization for interface calls. E.g. given a simple AOT-compiled C# program:

    using System.Runtime.CompilerServices;

    var bar = new Bar();
    var number = CallFoo(bar);

    Console.WriteLine(number);

    // Do not inline to prevent observing exact type
    [MethodImpl(MethodImplOptions.NoInlining)]
    static int CallFoo(Foo foo) {
        return foo.Number();
    }

    interface Foo {
        int Number();
    }

    class Bar: Foo {
        public int Number() => 42;
    }
On x86_64, 'CallFoo' compiles to:

    CMP byte ptr [RDI],DIL ;; null-check foo[0]
    MOV EAX,0x2a ;; set 42 to return value register
    RET
There is no interface call. In the above case, the linker reasons that throughout whole program only `Bar` implements `Foo` therefore all calls on `Foo` can be replaced with direct calls on `Bar`, which are then subject to other optimizations like inlining.

In fact, if we add and reference a second implementation of `Foo` - `Baz` which returns 8, `CallFoo` becomes

    ;; calculate the addr. of Bar's methodtable pointer
    LEA    RAX,[DevirtExample_Bar::vtable]
    MOV    ECX,0x8 ;; set ECX to 8
    MOV    EDX,0x2a ;; set EDX to 42
    ;; compare methodtable pointer of foo instance with Bar's
    CMP    qword ptr [RDI],RAX
    ;; set return register EAX to value of EDX, containing 42
    MOV    EAX,EDX
    ;; if comparison is false, set EAX to value of ECX containing 8 instead
    CMOVNZ EAX,ECX
    RET
Which is effectively 'return foo is Bar ? 42 : 8;'.

Despite my criticism of Go's capabilities, I am interested in how its implementation is evolving. I know it has the feature to manually gather a static PGO profile and then apply it to compilation which will insert guarded devirtualization fast-paths on interface calls, like what OpenJDK's HotSpot and .NET's JIT do automatically. But I was wondering whether it was doing any whole-program view or inter-procedural optimizations that can be very effective with "frozen world single static module" which both Go and .NET AOT compilations are.

EDIT: To answer my own question, I verified the same for Go. Given simple Go program:

    package main

    import (
        "fmt"
    )

    func main() {
        bar := &Bar{}
        num1 := callFoo(bar)

        fmt.Println(num1)
    }

    //go:noinline
    func callFoo(foo Foo) int {
        return foo.Number()
    }

    type Foo interface {
        Number() int
    }

    type Bar struct{}

    func (b *Bar) Number() int {
        return 42
    }
'callFoo' compiles to

    CMP        RSP,qword ptr [R14 + 0x10]
    JBE        LAB_0108ca68
    PUSH       RBP
    MOV        RBP,RSP
    SUB        RSP,0x8
    MOV        qword ptr [RSP + foo_spill.tab],RAX
    MOV        qword ptr [RSP + foo_spill.data],RBX
    MOV        RCX,qword ptr [RAX + 0x18] ;; load vtable slot?
    MOV        RAX,RBX
    NOP
    CALL       RCX ;; call the address loaded from the vtable?
    ADD        RSP,0x8
    POP        RBP
    RET
    LAB_0108ca68                                    XREF[1]:
    MOV        qword ptr [RSP + foo_spill.tab],RAX
    MOV        qword ptr [RSP + foo_spill.data],RBX
    CALL       runtime.morestack_noctxt                 
    MOV        RAX,qword ptr [RSP + foo_spill.tab]
    MOV        RBX,qword ptr [RSP + foo_spill.data]
    JMP        main.callFoo
It appears that no devirtualization takes place of this kind. Writing about this, it makes for an interesting thought experiment what it would take to introduce a CIL back-end for Go (including proper export of types, and what about structurally matched interfaces?) and AOT compile it with .NET.

[0]: VMs like OpenJDK and .NET make hardware exception-based null-checks. That is, a SIGSEGV handler is registered and then pointers that need to throw NRE or NPE either do so via induced loads from memory like above or just by virtue of dereferencing a field out of an object reference. If a pointer is null, this causes SIGSEGV, where then a handler looks if the address of the invalid pointer is within first, say, 64KiB of address space. If it is, the VM logic kicks in that recovers the execution state and performs managed exception handling such as running `finally` blocks and resuming the execution from the corresponding `catch` handler.

Re: Go is my hammer, and everything is a nail

#562
post #317

Earlier quoted context omitted.

I agree that list comprehensions aren't any easier to read. A proper streaming interface on the other hand lets you easily follow how the data is transformed: foo .stream() .filter(x -> x.contains("banned")) .collect(Collectors.toList()); As an aside, Go conflating lists and views irks me, in part due to what weird semantics it gives to append (e.g. if you have two disjunct slices and append an element to one slice,…

The problem with this is that people again get way to clever with it. it's not just stream -> filter -> collection, there will be a bunch of groupbys in there etc. If you have to debug or extend the functionality it's a nightmare to understand what all the intermediate representations are

Inspecting intermediate representations is trivial by just collecting them into a variable?

More complicated scenarios are exactly what streaming APIs excel at, by treating each step as a single transformation of data. Lack of a proper group by function is one of my classic examples for how Go forces you into an imperative style that's harder to understand at a glance.

Re: Go is my hammer, and everything is a nail

#563
post #467
post #377

Earlier quoted context omitted.

2012: Python is Awesome! 2014: Python is a great language, but there are a few pitfalls 2016: Python is a good language with the right IDE, tooling, and process. The people are pretty cool though. 2018: I like python, but I wish more people used type annotations. 2020: You know, metaclasses are freaking awesome! They saved me so much work! 2022: Why can't people code the most obvious solution in python? 2024: Celery!…

What's so bad about Celery?

Its powerful but good luck reading the source. Its a bit of a tangled over-engineered mess at this point and the reason there are a number of "newer" libraries to try and replace it. I could not recall specifics since it has been maybe a decade since I last used it but it works until it does not and then its incredibly hard to debug.

Its been a decade and maybe its gotten better but that sentiment is why a number of people do not want to use celery. I would also add that it was created in a different mindset of delayed job and I believe there are better patterns these days without as much complexity.

Re: Go is my hammer, and everything is a nail

#564
post #120
post #47

The author lists multiple reasons for this, but for me the biggest one is the first one: Go is good for almost everything . I have extremely good productivity when using Go. Once your project exceeds 100 lines it is usually even better than python. And yes, I am aware that Rustians did a survey where Rust was crowned as the most efficient language but in my reality (which may differ from yours) Go is simply the best…

Go is the only language I've ever felt highly productive working in. Oftentimes in other stacks I find myself in analysis paralysis on meta things that don't matter: - what design patterns/language features make sense to use - what is the best lib to accomplish X - how do you keep things up to date With Go, the language is so simple that it's pretty difficult to over engineer or write terse code. Everything you need…

Go has ruined all other languages for me. I really fell in love with Gleam recently and was trying to implement a fun side project in it. The problem is I really don’t have enough time to learn the intricacies of it, with a startup, two kids, etc. As soon as I have to look at some syntax and really _think_ about what it’s doing every time I look at it, I lose interest. I kept trying and eventually implemented it in Go much faster. And while doing it in Go I kept wishing I could just use actors and whatever to make it simpler but, is it really simpler?

Re: Go is my hammer, and everything is a nail

#565
post #267
post #120

Earlier quoted context omitted.

Go is the only language I've ever felt highly productive working in. Oftentimes in other stacks I find myself in analysis paralysis on meta things that don't matter: - what design patterns/language features make sense to use - what is the best lib to accomplish X - how do you keep things up to date With Go, the language is so simple that it's pretty difficult to over engineer or write terse code. Everything you need…

Sure, if everything one does is either CLI stuff, or UNIX daemons, containers, .... Because in the reign of graphics , GUI, GPGPU, HPC, HFT, ML, game engines,numeric analysis, ... there is hardly any library that really stands out.

wails, raylib, ebiten

Re: Go is my hammer, and everything is a nail

#566
post #94

People always under-estimate the cost of properly learning a language. At any given time I tend to have a "main go-to language". I typically spend 2-4 years getting to the point where I can say I "know" a language. Then I try to stick to it long enough for the investment to pay off. Usually 8-10 years. A surprising number of people think this is a very long time. It isn't. This is typically the time it takes to under…

And then you have the ever changing ecosystem, that can take years so sort out on it's own and must be constantly studied. E.G: if you arrive in Python right now, numpy is in version 2 and polars is stable. uv is all the rage, pydantic gained so much perf it's not even funny, toml is part of the stdlib and textual is looking very good. Type hints are much better than 2 years ago, htmx is used a lot in the web departm…

I would think the best option is to always be suspicious of hype, unless you understand why something is being hyped? I'd also argue its worth understanding where your stack falls down, so you know when you need to look for alternative stacks.

The other bit is it's worth understanding how your stack interacts with related stacks (to use your examples, does uv and pydantic using rust vs. being pure python cause issues), but your OS is also changing, are your tools still going to work?

Re: Go is my hammer, and everything is a nail

#567
post #419

Earlier quoted context omitted.

2012: Python is Awesome! 2014: Python is a pain in the butt to manage packages and dependencies, how the hell am I gonna deploy this? It's still a mess 10 years later unless you live deep in the ecosystem and know what third-party solutions du jour to manage that complexity. At least we have Docker now. Also let's not forget the Python 3 migration fiasco that lasted ~2008-2018 and I still find myself porting librarie…

Dependencies is a sign of weakness.

Ah yes, all applications should be a single very large source file that issues OS syscalls directly.

Re: Go is my hammer, and everything is a nail

#568
post #47

The author lists multiple reasons for this, but for me the biggest one is the first one: Go is good for almost everything . I have extremely good productivity when using Go. Once your project exceeds 100 lines it is usually even better than python. And yes, I am aware that Rustians did a survey where Rust was crowned as the most efficient language but in my reality (which may differ from yours) Go is simply the best…

It has not been my experience that Go is good for almost everything. On the contrary, it seems good at a couple very specific (though very common) niches: network services and cli utilities. But for most of what I do right now - data heavy work - it has not turned out to be very good (IMO). It really is just not better in any way to have to constantly write manual loops to do anything.

Re: Go is my hammer, and everything is a nail

#569

Earlier quoted context omitted.

Doing this allows you to mock out that implementation in unit tests.

A lot of times you want to be able to cmd+click on something and actually see what the hell the code actually does and not get dead-ended at an interface declaration.

Right click > view implementations

Re: Go is my hammer, and everything is a nail

#570

Earlier quoted context omitted.

I have a nice way of testing a language - I download a couple of projects written by beginner / mid-level developers and not having commercial dependencies and see how much effort it takes to get them running. Python is one of the worst performers (and Java is shockingly bad too, although a lot of that is down to the way the JVM/language have been mismanaged). At least unless you're comparing it to very low-level lan…

In which way do you believe the JVM/language has been mismanaged?

Oracle requiring commercial licensing for their JVM has made it so radioactive that my workplace firewalls "oracle.com" to prevent anyone from accidentally using it.
Post reply on HN