此代码如何生成内存对齐的切片?

How is this code generating memory aligned slices?

提问人:Mascarpone 提问时间:8/31/2022 最后编辑:Mascarpone 更新时间:8/31/2022 访问量:95

问:

我正在尝试在 linux 上进行直接 I/O,因此我需要创建内存对齐的缓冲区。我复制了一些代码来做到这一点,但我不明白它是如何工作的:

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))
}

我知道我正在生成一个比我需要的切片大两倍的切片,然后从中提取一个内存对齐的块,但该函数对我来说没有意义。Alignment

  • 该功能如何工作?Alignment
  • 如果我尝试该函数的中间步骤,我会得到不同的结果,为什么?我猜是因为观察它会改变它的记忆对齐(就像量子物理学:D一样)fmt.Println

编辑: 以 为例,我不再需要任何对齐方式:fmt.println

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))
}
Go 指针 IO 切片

评论


答:

1赞 icza 8/31/2022 #1

您的值为 2 的幂。在二进制表示中,它包含一个位,后跟满零:AlignSize1

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

分配的切片可能具有或多或少随机的内存地址,由二进制中随机跟随的 1 和 0 组成;或者更准确地说,是其后备数组的起始地址。make()

由于您分配了所需大小的两倍,因此可以保证后备数组将覆盖一个地址空间,该地址在中间的某个位置以与二进制表示形式一样多的零结尾,并且在数组中具有从此开始的空间。我们想找到这个地址。AlignSizeBlockSize

这就是函数的作用。它获取带有 的后备数组的起始地址。在 Go 中没有指针算术,所以为了做这样的事情,我们必须将指针转换为整数(当然还有整数算术)。为了做到这一点,我们必须将指针转换为不安全。指针:所有指针都可以转换为这种类型,并且可以转换为(这是一个大到足以存储指针值的未解释位的无符号整数),作为整数,我们可以执行整数运算。Alignment()&block[0]unsafe.Pointeruintptr

我们使用按位 AND 的值 .由于是 2 的幂(包含一个位后跟零),因此少一个数字是一个二进制表示充满 1 的数字,与尾随的零一样多。请参阅此示例:uintptr(AlignSize-1)AlignSize1AlignSize

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))

输出:

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

因此,结果是偏移量,如果减去 ,你会得到一个具有与自身一样多的尾随零的地址:结果与 的倍数“对齐”。&AlignSizeAlignSizeAlignSize

因此,我们将使用从 开始的切片部分,我们只需要:fileoffsetBlockSize

file = file[offset : offset+BlockSize]

编辑:

查看您尝试打印步骤的修改后的代码:我得到如下输出:

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

请注意,此处未更改任何内容。只需 fmt 包使用十六进制表示形式打印指针值,前缀为 。 值打印为整数,使用十进制表示形式。这些值相等:0xuintptr

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

还要注意其余的,因为在我的情况下已经是 的倍数,在二进制中它是 。00xc0000b600040961100000000000000000100001110000000000000

编辑#2:

当您用于调试部分计算时,这可能会更改转义分析,并可能更改切片的分配(从堆栈到堆)。这也取决于使用的 Go 版本。不要依赖在(已经)对齐的地址上分配切片。fmt.Println()AlignSize

有关更多详细信息,请参阅相关问题:

混合打印和 fmt。Println 和堆栈增长

为什么结构数组比较有不同的结果

空结构切片的地址

评论

1赞 Mascarpone 8/31/2022
惊人的答案,我只能佩服如此渊博的知识。三个问题: - 如何有意义?如何获取指向常量的指针?还是只是发出指针信号,而没有像那样实际提取它?- 为什么要更改值?例如:如果我打印中间步骤,那么神奇地所有块都已经对齐了 - 你能给我推荐一些可以加深这些知识的书籍或资源吗?uintptr(Alignsize-1)uintptr&file[0]fmt.Pritln
1赞 icza 8/31/2022
uintptr()是类型转换,因为 的左操作数也是 : 。 不会更改值。请出示您尝试过的代码。&uintptruintptr(unsafe.Pointer(&block[0]))fmt.Println()
1赞 icza 8/31/2022
而且你不能获取常量的地址,规范不允许,常量可能没有地址。有关详细信息,请参阅在 go 中查找常量地址
1赞 icza 8/31/2022
@Mascarpone 好吧,强制块大小可能是文件系统/操作系统的限制,但一般来说,这就是你使用切片的“有用”部分的方式:你只需切片它。
1赞 icza 8/31/2022
请参阅编辑的答案:打印的值相等,但基数不同(十六进制和十进制,因为类型不同:指针和)。fmt.Println()uintptr