I know what you're thinking. "Another performance comparison article? " But hear me out β€” I've got something spicier than your usual benchmarking blog post. Before you start rage-typing in the comments, let me explain why this is both absolutely true and completely ridiculous at the same time.

The Great Stack vs Heap Comedy Show

You see, both Go and Rust share a dirty little secret that nobody talks about at dinner parties: stack allocation is stupidly fast, and heap allocation is… well, not. It's like comparing a bicycle to a government bureaucracy β€” one gets you where you need to go instantly, the other involves filling out forms in triplicate.

Let me show you what I mean with some Go code that'll make your Rust-loving heart weep:

func sumValue() int {
    x := 10
    y := 20
    z := 30
    return x + y + z
}

Look at that beauty! Those variables x, y, and z are living their best life on the stack. When Go's runtime needs to allocate space for them, it literally just goes "Hey stack pointer, move over a bit, will ya?" and BAMβ€”memory allocated. It's like musical chairs, but the music never stops and everyone always has a seat.

Stack Memory (Lightning Fast ⚑)
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” ← SP before function call
β”‚  (free space)   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€ ← SP after allocating x, y, z
β”‚  z = 30         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  y = 20         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  x = 10         β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Allocation time:   SP += 12 bytes (basically instant)
Deallocation time: SP -= 12 bytes (even more instant)

The stack pointer (let's call him SP) is basically the world's laziest memory manager. He just points to the next available spot and says "Put your stuff here." When the function ends? He doesn't even clean up β€” he just forgets everything that happened. It's like having the memory of a goldfish, but in a good way.

How Zero-Cost Abstractions Cost Everything

Now, here's where it gets interesting. Rust developers love to brag about "zero-cost abstractions" and "memory safety without garbage collection." And they're right! But here's the thing β€” they often end up writing code like this:

fn sum_value() -> i32 {
    let x = Box::new(10);
    let y = Box::new(20);
    let z = Box::new(30);
    *x + *y + *z
}
Stack Memory (Our function frame):        Heap Memory (The expensive neighborhood):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                      β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ z: Box<i32>      β”‚ ───────────────────→ β”‚ [10] [?] [?] [20][?] [?] [?] [30] β”‚
β”‚ (ptr to heap)    β”‚                      β”‚   ↑            ↑              ↑   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€                      β”‚   β”‚            β”‚              β”‚   β”‚
β”‚ y: Box<i32>      β”‚ β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜   β”‚            β”‚              β”‚   β”‚
β”‚ (ptr to heap)    β”‚                          β”‚            β”‚              β”‚   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€                          β”‚            β”‚              β”‚   β”‚
β”‚ x: Box<i32>      β”‚ β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜            β”‚              β”‚   β”‚
β”‚ (ptr to heap)    β”‚                                       β”‚ Memory       β”‚   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                                       β”‚ fragments    β”‚   β”‚
                                                           β”‚ and unused   β”‚   β”‚
Allocation time: "Excuse me heap,                          β”‚ space        β”‚   β”‚
do you have room for my integer?"                          β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
(Heap allocator starts sweating)

"But wait!" you cry, "That's not idiomatic Rust! You're being unfair!" And you're absolutely right. A Rust programmer worth their salt would write:

fn sum_value() -> i32 {
    let x = 10;
    let y = 20;
    let z = 30;
    x + y + z
}

Which is… exactly the same performance profile as the Go version. Congratulations, we've achieved parity by writing identical code. I can practically hear the Rust evangelists breathing a sigh of relief.

The Real Plot Twist

Here's where my click-worthy title becomes both genius and completely misleading: when both languages use stack allocation, they're essentially doing the same thing. It's like two people bragging about how fast they can walk while they're both just… walking at normal human speed.

The stack pointer doesn't care about your language wars. Whether you're writing Go or Rust, SP just looks at you and says, "You want memory? Here's memory. SP += variable_size. Done. Next!"

But here's where it gets fun. Go's escape analysis is like that friend who's really good at party planning β€” it tries to keep everything local and on the stack. Sometimes it succeeds, sometimes it doesn't, but at least it's trying.

func createSlice() []int {
    // This might stay on the stack... or not
    slice := make([]int, 3)
    slice[0] = 1
    slice[1] = 2
    slice[2] = 3
    return slice  // Oops, escaping to heap!
}
What Go's escape analysis sees:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ "Hmm, this slice is being returned  β”‚
β”‚  outside the function... PANIC!"    β”‚
β”‚                                     β”‚
β”‚ Stack (what we wanted):             β”‚ Heap (what we got):
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”                     β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ slice data  β”‚ βœ—                   β”‚ β”‚ slice data      β”‚ βœ“
β”‚ β”‚ [1, 2, 3]   β”‚                     β”‚ β”‚ [1, 2, 3]       β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                     β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚                                     β”‚
β”‚ Result: Fast allocation becomes     β”‚
β”‚ slow allocation (thanks, escape     β”‚
β”‚ analysis... I guess?)               β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Rust, on the other hand, is like that overly meticulous friend who color-codes their calendar. It knows exactly where everything is going at compile time:

fn create_vec() -> Vec<i32> {
    // This is definitely heap-allocated, no surprises
    vec![1, 2, 3]
}

The Heap: Where Dreams Go to Die (Slowly)

When we venture into heap territory, both languages suddenly become mortal. The heap is like a giant warehouse where you have to walk around looking for the right-sized box, while the stack is like reaching into your pocket β€” you know exactly where everything is.

Here's Go doing heap things:

func createManyBoxes() []*int {
    boxes := make([]*int, 1000)
    for i := range boxes {
        value := i
        boxes[i] = &value  // Each allocation hits the heap
    }
    return boxes
}
The Heap Allocation Nightmare:

Stack (our function):           Heap (the expensive real estate):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ boxes: []*int    β”‚           β”‚ [0] [?] [1] [?] [?] [2] [3] [?] [4] ... β”‚
β”‚ [ptr1, ptr2...]  β”‚ ────────→ β”‚  ↑       ↑          ↑    ↑       ↑      β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚  β”‚       β”‚          β”‚    β”‚       β”‚      β”‚
                               β”‚ Each allocation = expensive search      β”‚
Allocator's internal monologue:β”‚ "Where can I put this integer?"         β”‚
"Gotta find 1000 spots..."     β”‚ "Is this spot free?"                    β”‚
"This is gonna take forever"   β”‚ "How about this one?"                   β”‚
                               β”‚ "Finally found one!"                    β”‚
                               β”‚ (Repeat 1000 times... 😴)               β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

And here's Rust doing the same expensive dance:

fn create_many_boxes() -> Vec<Box<i32>> {
    (0..1000)
        .map(|i| Box::new(i))  // Still hitting the heap, just more elegantly
        .collect()
}
Rust's Heap Adventure (Elegant but Still Expensive):

Stack:                          Heap:
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Vec<Box<i32>>    β”‚           β”‚ Box(0) Box(1) Box(2) ... Box(999)     β”‚
β”‚ [ptr, ptr, ...]  β”‚ ────────→ β”‚   ↓      ↓      ↓         ↓           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜           β”‚  [0]    [1]    [2]  ...  [999]        β”‚
                               β”‚                                       β”‚
Rust: "At least I'm being      β”‚ Allocator: "Still 1000 heap           β”‚
explicit about this expensive  β”‚ allocations, my friend"               β”‚
operation"                     β”‚                                       β”‚
                               β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Both of these functions are essentially saying "Hey allocator, I need 1000 individual memory slots, and I need you to find them for me right now." The allocator responds with the enthusiasm of a DMV employee on a Monday morning.

The Uncomfortable Truth

Here's what the performance gurus don't want you to know: most of the time, your choice of language matters way less than your choice of data structures and algorithms. I can write Go code that's slower than Rust, and I can write Rust code that's slower than Go. Watch me prove it:

Slow Go (fighting the runtime):

func inefficientSum() int {
    numbers := []*int{new(int), new(int), new(int)}
    *numbers[0] = 10
    *numbers[1] = 20
    *numbers[2] = 30
    
    sum := new(int)
    for _, num := range numbers {
        *sum += *num
    }
    return *sum
}
The "How to Make Go Cry" Approach:

Stack:                          Heap (unnecessary complexity):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”           β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ numbers: []*int  β”‚           β”‚                                    β”‚
β”‚ [ptr1,ptr2,ptr3] β”‚ ────────→ β”‚ [10] [?] [?] [20] [?] [30] [?] [0] β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€           β”‚  ↑             ↑       ↑       ↑   β”‚
β”‚ sum: *int        β”‚ β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                ↑       ↑       ↑   β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜                            β”‚       β”‚       β”‚   β”‚
                                            Our integers and sum    β”‚
Runtime: "Why are you doing this to me?"     are scattered around   β”‚
GC: "I have so much work to do now..."       like confetti          β”‚
                                            β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Fast Go (working with the runtime):

func efficientSum() int {
    numbers := [3]int{10, 20, 30}
    sum := 0
    for _, num := range numbers {
        sum += num
    }
    return sum
}
The "Go's Happy Place" Approach:

Stack (Everything lives here happily):
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ numbers: [3]int             β”‚
β”‚ β”Œβ”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”         β”‚
β”‚ β”‚ 10  β”‚ 20  β”‚ 30  β”‚ ←────── CPU cache loves this!
β”‚ β””β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”΄β”€β”€β”€β”€β”€β”˜         β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ sum: int = 0 β†’ 60           β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
Heap: (crickets chirping... it's empty!)
Runtime: "This is how we do it! πŸŽ‰"
GC: "What's a garbage collector? I'm unemployed!"
CPU: "Cache hits for days!"

The first version is practically begging the garbage collector to ruin its day, while the second version is so stack-friendly that SP barely has to move.

The Real Winner: Your Users' Patience

After years of watching developers argue about nanosecond differences while their applications take 3 seconds to load, I've come to a startling conclusion: the fastest code is the code that doesn't run at all.

But since we still need our programs to actually do things, the second-fastest code is the code that stays on the stack as much as possible, regardless of whether it's wearing Go's mascot gopher or Rust's crab costume.

In the End

So is my Go code faster than your Rust code? Well, that depends:

  • Are we both using stack allocation? Then we're probably tied, and arguing about it is like debating whether blue is faster than green.
  • Are we both hitting the heap frequently? Then we're both slow, and the real winner is whoever's code is easier to optimize.
  • Are you writing idiomatic code in your chosen language while I'm fighting against mine? Then your code is probably faster, and I should probably reconsider my life choices.

The uncomfortable truth is that the stack pointer doesn't read Hacker News. It doesn't care about your language preferences, your company's tech stack, or your strongly-held opinions about memory management. It just points to the next available memory address and does its job with the enthusiasm of a well-oiled machine.

So the next time someone tells you their language is definitively faster than yours, just smile and ask them whether they're talking about stack allocation or heap allocation. Then watch as they realize that the real performance battle isn't between languages β€” it's between good and bad memory management practices.

And remember: the fastest code is the friends we made along the way. Or something like that. I'm a programmer, not a philosopher.

None
taken by the author in Colonel Samuel Smith Park, Etobicoke, Ontario, Canada

P.S. If you're still convinced that your Rust is categorically faster than my Go (or vice versa), I have a bridge in Brooklyn I'd like to sell you. It's written in assembly and has zero-cost abstractions.