2

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))
}
Mascarpone
  • 2,516
  • 4
  • 25
  • 46

1 Answers1

1

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   : %22b\n", AlignSize)
fmt.Printf("AlignSize-1 : %22b\n", AlignSize-1)
fmt.Printf("x           : %22b\n", x)
fmt.Printf("result of & : %22b\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

icza
  • 389,944
  • 63
  • 907
  • 827
  • 1
    Amazing answer, I can only admire such depth of knowledge. Three questions: - how does `uintptr(Alignsize-1)` makes sense? How can I get a pointer to a constant? Or does `uintptr` simply signal a pointer, without actually extracting it like `&file[0]` does? - Why does `fmt.Pritln` change the values? e.g.: if I print the intermediate steps then magically all blocks are aligned already - Can you suggest me some books or resources where I can deepen this knowledge? – Mascarpone Aug 31 '22 at 08:38
  • 1
    `uintptr()` is a type conversion, needed because the left operand of `&` is also of type `uintptr`: `uintptr(unsafe.Pointer(&block[0]))`. `fmt.Println()` does not change the values. Please show the code you tried. – icza Aug 31 '22 at 08:42
  • 1
    And you can't acquire addresses of constants, not allowed by the spec, constants may not have an address. For details, see [Find address of constant in go](https://stackoverflow.com/questions/35146286/find-address-of-constant-in-go/35146856#35146856) – icza Aug 31 '22 at 08:42
  • I edited the answer with an example where I don't need memory alignment, simply because I printed the intermediate steps (without changing the slice). Really feels like quantum physics: simple observation without interaction changes the output. – Mascarpone Aug 31 '22 at 08:48
  • Also: unfortunately the write example doesn't work :( if I try ` n, readErr := unix.Write(fd, file[:32])` or any other variation which is not the size of a block (512), I get an error. But without `o_direct` I can write files without `nul` at the end. – Mascarpone Aug 31 '22 at 08:49
  • 1
    @Mascarpone OK, the enforced block size may be a file system / os limitation, but in general this is how you use the "useful" part of a slice: you simply slice it. – icza Aug 31 '22 at 08:50
  • 1
    See edited answer regarding `fmt.Println()`: the printed values are equal, but are in different base (hexa and decimal, due to being different types: pointer and `uintptr`). – icza Aug 31 '22 at 08:57
  • 1
    maybe we misunderstood each other. Simply doing fmt.Println(&file[0]) means I don't need to align memory anymore, the new block size is 4096 and not 4096*2, no need to call Alignment anymore, it just magically gets aligned. Without that Println the same code crashes. Why? is it a more efficient way to align memory? – Mascarpone Aug 31 '22 at 09:04
  • 1
    @Mascarpone That may be due to escape analysis and relocation to the heap. See edited answer. – icza Aug 31 '22 at 09:11
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/247705/discussion-between-mascarpone-and-icza). – Mascarpone Aug 31 '22 at 09:31