Home > Enterprise >  How is this code generating memory aligned slices?
How is this code generating memory aligned slices?

Time:08-31

I'm trying to do direct i/o on linux, so I need to create memory aligned buffers. I copied some code to do it, but I don't understand how it works:

package main

import (
    "fmt"
    "golang.org/x/sys/unix"
    "unsafe"
    "yottaStore/yottaStore-go/src/yfs/test/utils"
)

const (
    AlignSize = 4096
    BlockSize = 4096
)

// Looks like dark magic
func Alignment(block []byte, AlignSize int) int {
    return int(uintptr(unsafe.Pointer(&block[0])) & uintptr(AlignSize-1))
}

func main() {

    path := "/path/to/file.txt"
    fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECT, 0666)
    defer unix.Close(fd)

    if err != nil {
        panic(err)
    }

    file := make([]byte, 4096*2)

    a := Alignment(file, AlignSize)

    offset := 0
    if a != 0 {
        offset = AlignSize - a
    }

    file = file[offset : offset BlockSize]


    n, readErr := unix.Pread(fd, file, 0)
    
    if readErr != nil {
        panic(readErr)
    }

    fmt.Println(a, offset, offset utils.BlockSize, len(file))
    fmt.Println("Content is: ", string(file))
}

I understand that I'm generating a slice twice as big than what I need, and then extracting a memory aligned block from it, but the Alignment function doesn't make sense to me.

  • How does the Alignment function works?
  • If I try to fmt.Println the intermediate steps of that function I get different results, why? I guess because observing it changes its memory alignment (like in quantum physics :D)

Edit: Example with fmt.println, where I don't need any more alignment:

package main
import (
    "fmt"
    "golang.org/x/sys/unix"
    "unsafe"
)

func main() {

    path := "/path/to/file.txt"
    fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECT, 0666)
    defer unix.Close(fd)

    if err != nil {
        panic(err)
    }

    file := make([]byte, 4096)

    fmt.Println("Pointer: ", &file[0])

    n, readErr := unix.Pread(fd, file, 0)

    fmt.Println("Return is: ", n)

    if readErr != nil {
        panic(readErr)
    }

    fmt.Println("Content is: ", string(file))
}

CodePudding user response:

Your AlignSize has a value of a power of 2. In binary representation it contains a 1 bit followed by full of zeros:

fmt.Printf("%b", AlignSize) // 1000000000000

A slice allocated by make() may have a memory address that is more or less random, consisting of ones and zeros following randomly in binary; or more precisely the starting address of its backing array.

Since you allocate twice the required size, that's a guarantee that the backing array will cover an address space that has an address in the middle somewhere that ends with as many zeros as the AlignSize's binary representation, and has BlockSize room in the array starting at this. We want to find this address.

This is what the Alignment() function does. It gets the starting address of the backing array with &block[0]. In Go there's no pointer arithmetic, so in order to do something like that, we have to convert the pointer to an integer (there is integer arithmetic of course). In order to do that, we have to convert the pointer to unsafe.Pointer: all pointers are convertible to this type, and unsafe.Pointer can be converted to uintptr (which is an unsigned integer large enough to store the uninterpreted bits of a pointer value), on which–being an integer–we can perform integer arithmetic.

We use bitwise AND with the value uintptr(AlignSize-1). Since AlignSize is a power of 2 (contains a single 1 bit followed by zeros), the number one less is a number whose binary representation is full of ones, as many as trailing zeros AlignSize has. See this example:

x := 0b1010101110101010101
fmt.Printf("AlignSize   : "b\n", AlignSize)
fmt.Printf("AlignSize-1 : "b\n", AlignSize-1)
fmt.Printf("x           : "b\n", x)
fmt.Printf("result of & : "b\n", x&(AlignSize-1))

Output:

AlignSize   :          1000000000000
AlignSize-1 :           111111111111
x           :    1010101110101010101
result of & :           110101010101

So the result of & is the offset which if you subtract from AlignSize, you get an address that has as many trailing zeros as AlignSize itself: the result is "aligned" to the multiple of AlignSize.

So we will use the part of the file slice starting at offset, and we only need BlockSize:

file = file[offset : offset BlockSize]

Edit:

Looking at your modified code trying to print the steps: I get an output like:

Pointer:  0xc0000b6000
Unsafe pointer:  0xc0000b6000
Unsafe pointer, uintptr:  824634466304
Unpersand:  0
Cast to int:  0
Return is:  0
Content is: 

Note nothing is changed here. Simply the fmt package prints pointer values using hexadecimal representation, prefixed by 0x. uintptr values are printed as integers, using decimal representation. Those values are equal:

fmt.Println(0xc0000b6000, 824634466304) // output: 824634466304 824634466304

Also note the rest is 0 because in my case 0xc0000b6000 is already a multiple of 4096, in binary it is 1100000000000000000100001110000000000000.

Edit #2:

When you use fmt.Println() to debug parts of the calculation, that may change escape analysis and may change the allocation of the slice (from stack to heap). This depends on the used Go version too. Do not rely on your slice being allocated at an address that is (already) aligned to AlignSize.

See related questions for more details:

Mix print and fmt.Println and stack growing

why struct arrays comparing has different result

Addresses of slices of empty structs

  • Related