Previous Entry Share Next Entry
Slices in D vs Go
anime
he_the_great

This article is here to complement the array slice articles for D and Go (another). It will be increasingly common for people to come with an understanding of the approach used in one of these languages, this article should help to point out the major differences.

Slices are a way of viewing a continuous block of memory, a window over an array. This is true of both languages, the difference resides in how one can grow and shrink this window. D's philosophy is to be memory safe by default, a slice in D will never stomp memory it doesn't own. For different definitions of "own" Go slices will not stomp memory it doesn't own.

In D the underlying array is owned by the run-time, not the slice. In Go every slice owns the array it is sliced over, or at least it owns from where the slice starts to where the underlying array ends.

The code bellow is intended to append to the previous for the given language. So you can start with a compiling D program and a failing Go and you can follow along with IDEOne for the language you don't have installed.

dlang
import std.stdio;

void main() {
    // Insert appending blocks here
}

golang
package main
import "fmt"

func main(){
    // Insert appending blocks here
}

We'll start with simple capacity reservation.

dlang
    int[] original;
    original.reserve(5);
    writeln("orig cap: ", original.capacity); // 7
    writeln("orig len: ", original.length); // 0

golang
    original := make([]int05);
    fmt.Println("orig cap:"cap(original)); // 5
    fmt.Println("orig len:"len(original)); // 0

Making a slice and reserving memory will increase the capacity while keeping the slices length the same. We won't observe any difference in Go. But to discuss behavior we'll need some elements.

dlang
    auto ptr = original.ptr;
    original ~= 0;
    original ~= 1;
    original ~= 2;
    writeln("orig cap: ", original.capacity); // 7
    writeln("orig len: ", original.length); // 3
    assert(ptr == original.ptr);

    auto slice = original[1..$];
    writeln("slice cap: ", slice.capacity); // 6
    writeln("slice len: ", slice.length); // 2
    original[0]++;
    slice[0]++;
    slice[1]++;
    writeln("orig: ", original); // [1, 2, 3]

Building the array by appending continues to use the same memory allocated, since there is capacity in the underlying array. Manipulating the memory from either slice is observed by the other.

golang
    original = append(original, 0)
    original = append(original, 1)
    original = append(original, 2)
    fmt.Println("orig cap:"cap(original)) // 5
    fmt.Println("orig len:"len(original)) // 3

    slice := original[1:]
    fmt.Println("slice cap:"cap(slice)) // 4
    fmt.Println("slice len:"len(slice)) // 2
    original[0]++
    slice[0]++
    slice[1]++
    fmt.Println("orig:", original) // [1, 2, 3]

Go and D have the exact same behavior. The capacity reflects to the end of the underlying array in both the original and the slice. Since the slice does not contain the first element of the original, the capacity is lower by one. What is expected to happen when we append to original and to slice? Can you guess the behavior of the language you're unfamiliar with?

dlang
    slice ~= 4;
    writeln("orig cap: ", original.capacity); // 0
    writeln("orig len: ", original.length); // 3
    writeln("slice cap: ", slice.capacity); // 6
    writeln("slice len: ", slice.length); // 3
    writeln("slice: ", slice); // [2, 3, 4]

    original ~= -1;
    writeln("orig: ", original); // [1, 2, 3, -1]
    writeln("slice: ", slice); // [2, 3, 4]

golang
    slice = append(slice, 4)
    fmt.Println("orig cap:"cap(original)) // 5
    fmt.Println("orig len:"len(original)) // 3
    fmt.Println("slice cap:"cap(slice)) // 4
    fmt.Println("slice len:"len(slice)) // 3
    fmt.Println("slice:", slice) // [2, 3, 4]

    original = append(original, -1)
    fmt.Println("orig:", original) // [1, 2, 3, -1]
    fmt.Println("slice:", slice) // [2, 3, -1]

In D we observe that capacity has been modified on original, it was set to zero. Go however maintains that both original and slice still own the end capacity. From this we observe that D has maintained the value we stored into slice, while Go has stomped on our changes. This is one of the reasons D's dynamic arrays are accused of hidden allocations. This is also true of Go unless you're watching the capacity closely (which is a valid approach for D too). That said...

dlang
    slice = original[1..$];
    slice ~= 4;
    writeln("slice: ", slice); // [2, 3, -1, 4]
    assumeSafeAppend(original);
    original ~= -1;
    writeln("orig: ", original); // [1, 2, 3, -1, -1]
    writeln("slice: ", slice); // [2, 3, -1, -1]

Immutability

For whatever reason Go can get away with this approach, let me explain why D can't. With D, types can be marked immutable, meaning that the value can never be changed. This is different from marking the type const, which just prevents the reference from changing the value, it can be changed elsewhere in the code.

We'll take the most common, immutable(char)[]. Strings are a very common place to use slicing (probably because they are so common).

dlang
    auto str = "Hello World";
    const(char)[] strSlice = str[0..5];
    strSlice ~= "/bye";
    writeln(str); // "Hello World"

To guarantee that the immutable values in the array aren't changed it must guarantee that it won't stomp the arrays when another slice appends data.

Go Source
D Source

  • 1
Go 1.2 allows setting the nee slice's capacity, thus allowing you to protect against append changing "dual owned" parts.

I actually didn't really considered the inability to set a specific capacity. But it is good Go has provided such control.

  • 1
?

Log in