Hacker News new | ask | show | jobs
by weberc2 2882 days ago
It’s not native vs VM, but rather “has stack semantics/value types” vs “no stack semantics/value types”. In particular, OCaml’s standard implementation is a native, not VM.

Also worth calling out Go, which is rather unique in that it has stack semantics but it also has a garbage collector, so it’s kind of the best of both worlds in terms of ease of writing correct, performant code.

4 comments

Go is not rather unique in having GC and stack semantics, there are plenty of languages that have it, all the way back to Mesa/Cedar and CLU.
I should have been more clear I guess; I was comparing it to other popular languages. Few have value types and many that do (like C#) regard them as second-class citizens.
But go has an imprecise GC (in reference implementation) or stack maps (in gccgo), so the GC overhead is rather huge. It also lacks of compaction, so cache misses are not that good too.
Not sure what you mean by imprecise, but Go’s GC does trade throughput for latency. The overhead still isn’t huge if only because there is so much less garbage than in other GC languages. I’m also surprised by your cache misses claim; Go has value types which are used extensively in idiomatic code so generally the cache properties seem quite good—maybe my experience is abnormal?
>Not sure what you mean by imprecise

It's a rigid term:

https://en.wikipedia.org/wiki/Tracing_garbage_collection#Pre...

perf shows how much time does GC eat, and that's quite a lot. Thus in the majority of benchmarks go lags behind java or on par with it at best.

>there is so much less garbage than in other GC languages

That is not true since strings and interfaces are heap allocated thus the only stack allocated objects are numbers and very simple structs (i.e. which contains only numbers), so you would have a lot of garbage unless you are doing a number crunching, which could be easily optimized by inlining and register allocation anyway.

> It's a rigid term

Ah, neat! I learned something. :)

You’re mistaken about only numbers and simple structs being stack allocated. All structs are stack allocated unless they escape, regardless of their contents. Further, arrays and constant-sized slices may also be stack allocated. I’m also pretty sure interfaces are only heap allocated if they escape; in other words, if you put a value in an interface and it doesn’t escape, there shouldn’t be an allocation at all.

Both arrays and interfaces are heap allocated. Slice is just a pointer to a heap allocated array.

Structure could be stack allocated, but any of it's fields would not if there is anything but a number.

A trivial example:

https://segment.com/blog/allocation-efficiency-in-high-perfo...

    func main() {
            x := 42
            fmt.Println(x)
    }

    ./main.go:7: x escapes to heap
So a trivial interface cast leads to allocation.
Looks like you're right about interfaces (full benchmark source code: https://gist.github.com/weberc2/87d2fdc379065a2765d1c9f490ad...)!

    BenchmarkEscapeInterface-4        50000000   33.3 ns/op  8 B/op  1 allocs/op
    BenchmarkEscapeConcreteValue-4    200000000  9.45 ns/op  0 B/op  0 allocs/op
    BenchmarkEscapeConcretePointer-4  100000000  10.0 ns/op  0 B/op  0 allocs/op
But arrays are stack allocated:

    BenchmarkEscapeArray-4  50000000   21.3 ns/op  0 B/op  0 allocs/op
And structs are stack allocated, as are their fields--even fields that are structs, slices, and strings!:

    BenchmarkEscapeStruct-4  100000000  12.8 ns/op  0 B/op  0 allocs/op

The code:

    type Inner struct {
    	Slice  []int
    	String string
    	Int    int
    }
    
    type Struct struct {
    	Int    int
    	String string
    	Nested Inner
    }
    
    func (s Struct) AddThings() int {
    	return s.Int + len(s.String) + len(s.Nested.Slice) + len(s.Nested.String) +
    		s.Nested.Int
    }
    
    func BenchmarkEscapeStruct(b *testing.B) {
    	for i := 0; i < b.N; i++ {
    		s := Struct{
    			Int:    42,
    			String: "Hello",
    			Nested: Inner{
    				Slice:  []int{0, 1, 2},
    				String: "World!",
    				Int:    42,
    			},
    		}
    		_ = s.AddThings()
    	}
    }
Regarding your last point, Crystal has the same features as go in that regard, while at the same time being vastly more expressive. This mostly due to the standard library in Crystal being so nice for work with collections (which perhaps isn't surprising as the APIs are heavily influenced by Ruby). Blocks being overhead free is another necessary part for this to work well.
Yeah, I often find myself wishing Go's type system were a bit better, but the reason I prefer it is because it's fast, easy to reason about, and the tooling/deployment stories are generally awesome (not always though--e.g., package management). So far I'm only nominally familiar with Crystal; I'll have to look into it sometime.
.NET is another example of value types in a garbage collected language. It’s also somewhat unique afaik in doing so within a VM.
Definitely. I’m sad that they’re not more idiomatic in C#. I definitely prefer values and references over OOP class objects.