Go: math/bits:一个整数位旋转库

创建于 2017-01-11  ·  168评论  ·  资料来源: golang/go

之前在https://github.com/golang/go/issues/17373https://github.com/golang/go/issues/10757上的讨论

抽象的

该提案引入了一组用于整数位旋转的 API。

背景

该提案引入了一组用于整数位旋转的 API。 对于此提案,我们对以下功能感兴趣:

  • ctz - 计算尾随零。
  • clz - 计算前导零; 日志_2。
  • popcnt - 计数人口; 汉明距离; 整数奇偶校验。
  • bswap - 颠倒字节顺序。

这些功能是通过调查挑选出来的:

我们将自己限制在这四个功能上,因为其他摆弄
使用建议的库实现技巧非常简单,
或已经可用的 Go 结构。

我们找到了选定的旋转函数子集的实现
在许多包中,包括运行时、编译器和工具:

| 包| cz | ctz | popcnt | 交换|
| --- | --- | --- | --- | --- |
| 数学/大| X | X | | |
| 运行时/内部/系统| X | X | | X |
| 工具/容器/intsets | X | | X | |
| cmd/编译/内部/ssa | X | | X | |
| code.google.com/p/intmath | X | X | | |
| github.com/hideo55/go-popcount | | | X (asm) | |
| github.com/RoaringBitmap/roaring | | X | X (asm) | |
| github.com/tHinqa/bitset | X | X | X | |
| github.com/willf/bitset | X | | X (asm) | |
| gopl.io/ch2/popcount | | | X | |
| GCC 内置函数 | X | X | X | X |

许多其他包实现了这些功能的一个子集:

同样,硬件供应商也认识到了重要性
此类功能并包括机器级支持。
如果没有硬件支持,这些操作非常昂贵。

| 拱| cz | ctz | popcnt | 交换|
| --- | --- | --- | --- | --- |
| AMD64 | X | X | X | X |
| 手臂 | X | X | ? | X|
| ARM64 | X | X | ? | X|
| S390X | X | X | ? | X|

除了 popcnt 之外,所有位处理函数都已经由 runtime/internal/sys 实现,并得到编译器的特殊支持,以“帮助获得最佳性能”。 但是,编译器支持仅限于运行时包,其他 Golang 用户必须重新实现这些函数的较慢变体。

提议

我们引入了一个带有以下外部 API 的新标准库math/bits ,以提供 clz、ctz、popcnt 和 bswap 函数的编译器/硬件优化实现。

package bits

// SwapBytes16 reverses the order of bytes in a 16-bit integer.
func SwapBytes16(uint16) uint16
// SwapBytes32 reverses the order of bytes in a 32-bit integer.
func SwapBytes32(uint32) uint32
// SwapBytes64 reverses the order of bytes in a 64-bit integer.
func SwapBytes64(uint64) uint64

// TrailingZeros32 counts the number of trailing zeros in a 32-bit integer, and if all are zero, then 32.
func TrailingZeros32(uint32) uint
// TrailingZeros64 counts the number of trailing zeros in a 64-bit integer, and if all are zero, then 64.
func TrailingZeros64(uint64) uint

// LeadingZeros32 counts the number of trailing zeros in a 32-bit integer, and if all are zero, then 32.
func LeadingZeros32(uint32) uint
// LeadingZeros64 counts the number of trailing zeros in a 64-bit integer, and if all are zero, then 64.
func LeadingZeros64(uint64) uint

// Ones32 counts the number of bits set in a 32-bit integer.
func Ones32(uint32) uint
// Ones64 counts the number of bits set in a 64-bit integer.
func Ones64(uint64) uint

基本原理

该提案的替代方案是:

  • 位旋转函数在编译器不支持的外部库中实现。 这种方法有效并且是当前的事态。 运行时使用编译器支持的方法,而 Golang 用户继续使用较慢的实现。
  • 编译器支持外部库。 鉴于我们希望这个库取代 runtime/internal/sys,这意味着这个库必须与编译器锁定并存在于标准库中。

兼容性

此提案不会更改或破坏任何现有的 stdlib API,并且符合兼容性指南。

执行

SwapBytes、TrailingZeros 和 LeadershipZeros 已经实现。 唯一缺少的功能是 Ones,它可以与其他功能类似地实现。 如果该提案被接受,则可以及时在 Go1.9 中实施。

未解决的问题(如果适用)

名字很难,自行车棚在评论中。

请在评论中建议要包含的其他功能。 理想情况下,请包括在 stdlib(例如 math/big)、工具或流行包中使用此类函数的位置。

到目前为止,已经提出并正在考虑以下功能:

  • 向左旋转/向右旋转

    • 优点: &63 不再需要将带有非常量参数的旋转编译为 x86、x86-64 上的单个指令。

    • 缺点:实现和内联非常短/简单。

    • 使用:crypto/ 使用由编译器正确处理的常量旋转。

  • 反向位

    • 优点:?

    • 缺点:?

    • 用过的: ?

  • 带进位返回的加/减

    • 优点:否则贵

    • 缺点:?

    • 使用:数学/大

历史

14.Jan:澄清了参数为0时TrailingZeros和LeadingZeros的输出。
14.Jan:重命名方法:CountTrailingZeros -> TrailingZeros,CountLeadingZeros -> LeadershipZeros,CountOnes -> Ones。
13.Jan:固定架构名称。
11.Jan:初步提案向公众开放。

FrozenDueToAge Proposal-Accepted

最有用的评论

编写代码的人想要向左旋转或向右旋转。 他们不想向左或向右旋转,具体取决于。 如果我们只提供rotate-left,那么任何想要向右旋转的人都必须编写一个表达式来将他们的向右旋转转换为向左旋转。 这是一种计算机可以做的很简单但程序员会犯错误的表达方式。 为什么要让程序员自己写呢?

所有168条评论

@brtzsnr也许您应该按照提案流程步骤中的概述将此文档提交给提案回购?

由于它已经按照模板进行了降价,因此应该很容易复制粘贴到 CL 创建文件 design/18616-bit-twiddling.md (或其他)。

@cespare来自https://github.com/golang/proposal “如果作者想写一个设计文档,那么他们可以写一个”。 它开始是一个设计文档,如果有强烈的感觉我应该提交这个,我完全没问题。

我可以接受,它在许多算法库中使用的功能足够普遍,数学/位似乎是一个合适的地方。

(一方面, math/big 也实现了 nlz (== clz)。)

可能有一些关于名称的自行车脱落。 我个人更喜欢函数说它们返回什么而不是它们做什么; 这反过来可能会导致更短的名称。 例如:

bits.TrailingZeros64(x)而不是bits.CountTrailingZeros64(x)

等等。

该提案似乎非常清晰和最小 - 设计文档似乎有点矫枉过正。 我认为此时使用 CL 会更合适。

(这是一个带有 API 和基本实现的 CL - 为了代替设计文档进行讨论。我们仍然需要决定是否应该接受这个提议。)

@brtzsnr已经编写了设计文档:它在问题描述中,并且遵循模板。 我认为将这些文档全部放在一个位置具有一定的价值。

硬件支持表中列出的最后一个架构是“BSWAP”——错别字?

谢谢你写这个。

ctz 和 clz 的文档字符串应指定传递 0 时的结果。

我也更喜欢(例如)TrailingZeros32 到 CountTrailingZeros32。 我也会对 Ctz32 感到满意。 它简洁明了,大多数人都熟悉,其他人也很容易用谷歌搜索。

谢谢你的提议。
我只想补充一点,我们可能也想关注使用,而不是只关注说明。 例如, @dr2chase曾经发现编译器/汇编器中有几个log2函数。 它接近CLZ但不一样。 像这样的函数可能也应该在 bit twiddling 包中。 并且可能还包括与这些说明无关的有用功能。

我们为由定义的所有位旋转原语提供一个包如何?
黑客的喜悦?

在设计包的时候,我们不需要考虑功能是否
可以是内在化的,也可以不是。 优化可以稍后进行。 那是,
不要让低级实现控制上级包
界面。 我们想要一个好的封装接口,即使有些不能
映射到单个指令。

@minux幸运的是,到目前为止我需要的每一

遵循 Hacker's Delight 的优点是我们不需要浪费时间争论名称。

我想添加以下内容:

ReverseBits(用于 uint32 和 uint64)
RotateLeft/Right(可以通过两个移位内联扩展,但编译器
由于移位范围问题,不能总是进行转换)

也许也是加减的两种结果形式? 例如

func AddUint32(x, y, Carryin uint32) (carryout, sum uint32) // 进位必须
为 0 或 1。
uint64 也类似。

和 SqrtInt。

我的许多建议无法在一条指令中实现,但是
这就是重点:我们想要一个很好的玩弄包,而不仅仅是一个
内部封装。 不要让硬件限制高级包
界面。

相关地,用于检查加法/乘法是否会溢出的函数。

一个相关的数据点:已经为更快的 cgo 提交了各种问题。 在一个这样的例子中(提案 #16051),快速实现 bsr/ctz/etc 的事实。 可能会发生被提到希望减少编写 go 的人倾向于使用 cgo 的用例集。

@aclements于 2016 年 7 月 1 日发表评论:

@eloff ,已经进行了一些讨论(尽管据我所知没有具体的建议)为 popcount 和 bsr 之类的东西添加函数,这些函数将被编译为支持的内在函数(例如今天的 math.Sqrt)。 在 1.7 中,我们正在运行时尝试这个,现在它在 amd64 SSA 上有一个 ctz 内在可用。 显然,这并不能解决整个问题,但它会削弱在低开销环境中使用 cgo 的另一个理由。

很多人(包括我自己)都因为表演而被吸引去,所以像这个当前的比特摆弄提案这样的事情会有所帮助。 (是的,cgo 现在在 1.8 中也更快了,这也很好)。

RotateLeft/Right(可以通过两个移位内联扩展,但编译器
由于移位范围问题,不能总是进行转换)

@minux你能详细说明是什么问题吗?

@brtzsnr :我认为 Minux 指的是当你写(x << k) | (x >> (64-k)) ,你知道你在使用0 <= k < 64 ,但是编译器无法读懂你的想法,这显然不是可以从代码中推导出来。 如果我们有这个功能

func leftRot(x uint64, k uint) uint64 {
   k &= 63
   return (x << k) | (x >> (64-k))
}

然后我们可以确保(通过 &63)编译器知道 k 的范围是有界的。
所以如果编译器不能证明输入是有界的,那么我们需要一个额外的 AND。 这比根本不生成旋转组件要好。

2017 年 1 月 13 日星期五晚上 10:37,Keith Randall通知@github.com
写道:

@brtzsnr https://github.com/brtzsnr :我认为 Minux 指的是什么
to 是当你写 (x << k) | (x >> (64-k)),你知道你在使用 0
<= k < 64,但是编译器读不懂你的心思,而且不是很明显
可以从代码中推导出来。 如果我们有这个功能

func leftRot(x uint64, k uint) uint64 {
k &= 63
返回 (x << k) | (x >> (64-k))
}

然后我们可以确保(通过 &63)编译器知道范围
k 是有界的。
所以如果编译器不能证明输入是有界的,那么我们需要一个额外的
和。 这比根本不生成旋转组件要好。

对。 如果我们定义 RotateLeft 和 RotateRight 函数,我们可以形式化
定义函数左/右旋转 k 位(无论 k 是什么)。 这是
类似于我们的移位操作是如何定义的。 而这个定义也
很好地映射到实际的旋转指令(不像班次,我们的更多
直观的定义需要在某些架构上进行比较)。

blosc压缩库使用的字节和位混洗(和解混)函数怎么样? 幻灯片(洗牌从幻灯片 17 开始)。 这些函数可以被 SSE2/AVX2 加速。

2017 年 1 月 13 日星期五晚上 11:24,opennota [email protected]写道:

blosc 使用的字节和位改组函数如何
https://github.com/Blosc/c-blosc压缩库? 幻灯片
http://www.slideshare.net/PyData/blosc-py-data-2014 (改组
从幻灯片 17) 开始。 这些函数可以被 SSE2/AVX2 加速。

SIMD 是一个更大的问题,它超出了这个包的范围。 它是

17373。

当前提议的函数有一个 Go 原生实现,比优化的要大得多,而且不成比例地更昂贵。 另一方面,rotate 很容易以编译器可以识别的方式编写内联。

@minux以及其他所有人:您知道在何处使用旋转左/右旋转和非常量的旋转位吗? 例如,crypto/sha256 使用旋转,但位数不变。

旋转很容易以编译器可以识别的方式编写内联。

对于熟悉编译器内部结构的人来说很容易。 将它放在 math/bits 包中,每个人都可以轻松使用。

你知道在什么地方使用旋转左/右旋转的位数非常多吗?

以下是#9337 中的示例:

https://play.golang.org/p/rmDG7MR5F9

在每次调用中,它每次都是固定数量的旋转位,但函数本身当前没有内联,因此它在没有任何旋转指令的情况下进行编译。 数学/位库函数在这里肯定会有所帮助。

2017 年 1 月 14 日星期六上午 5:05,Alexandru Moșoi通知@github.com
写道:

当前提议的函数具有更大的 Go 原生实现
并且不成比例地比最佳的贵。 另一方面旋转
很容易以编译器可以识别的方式编写内联。

正如我在这个问题中多次强调的那样,这不是正确的方法
设计一个 Go 包。 它与底层硬件有太多联系。 我们什么
want 是一个很好的小包,通常很有用。 无论
功能可以扩展成单条指令是无关紧要的
因为 API 接口是众所周知的并且通常很有用。

@minux https://github.com/minux以及其他所有人:你知道吗
其中旋转左/右与非常量的旋转位一起使用?
例如,crypto/sha256 使用旋转,但位数不变。

即使在实际问题中循环的位数是恒定的,
编译器可能无法看到。 例如,当存储移位计数时
在数组中,或隐藏在循环计数器中,甚至是未内联的调用者
功能。

一个使用可变数量旋转的简单例子是一个有趣的例子
popcount的实现:
// https://play.golang.org/p/ctNRXsBt0z

func RotateRight(x, k uint32) uint32
func Popcount(x uint32) int {
    var v uint32
    for i := v - v; i < 32; i++ {
        v += RotateRight(x, i)
    }
    return int(-int32(v))
}

@josharian如果 rot 没有内联,这个例子看起来像是一个糟糕的内联决定。 您是否尝试将函数编写为func rot(x, n)而不是rot = func(x, n)

@minux :我同意你的看法。 我不是想将 API 绑定到特定的指令集; 硬件支持是一个不错的奖励。 我的主要重点是在真实代码(而不是玩具代码)中找到用法来理解上下文,什么是最好的签名以及提供每个人最喜欢的功能有多重要。 如果我们现在不正确,兼容性承诺将在以后咬我们。

例如:add 和carry return 的签名应该是什么? Add(x, y uint64) (c, s uint64) ? 看看math/big我们可能也需要Add(x, y uintptr) (c, s uintptr)

如果 rot 没有内联,这个例子看起来像是一个糟糕的内联决定。

是的。 这是抱怨内联的错误的一部分。 :)

您是否尝试将函数编写为 func rot(x, n) 而不是 rot = func(x, n)?

这不是我的代码——这是重点的一部分。 无论如何,这是合理的代码。

最好保证(在包文档中?)旋转和字节交换函数是恒定时间操作,以便它们可以安全地用于加密算法。 其他功能也可能需要考虑。

2017 年 1 月 19 日星期四上午 11:50,Michael Munday通知@ github.com
写道:

最好保证(在包文档中?)
旋转和字节交换函数是恒定时间操作,因此它们
可以安全地用于加密算法。 可能需要考虑的事情
也适用于其他功能。

字节交换的琐碎实现是常数时间,但如果底层
架构不提供可变移位指令,这将很难
以保证恒定的时间轮换实现。 也许 Go 永远不会运行
不过在这些架构上。

也就是说,底层证券也有不可忽视的可能性
微架构使用多周期移位器,我们不能保证
在这些实现上进行恒定的时间轮换。

如果需要严格的恒定时间,也许唯一的方法是编写程序集
(即使在那种情况下,它也做出了强有力的假设,即所有使用的
指令本身是常数时间,这隐含地取决于
微架构。)

虽然我理解这种保证的必要性,但实际上超出了
我们的控制。

我倾向于同意@minux。 如果你想要恒定时间的加密原语,它们应该存在于加密/微妙中。 在已经验证了这些实现的平台上,crypto/subtle 可以轻松重定向到数学/位。 如果需要较慢但恒定时间的实现,他们可以做其他事情。

这似乎值得做。 离开@griesemer@randall77进行审查。 下一步似乎是将设计文档签入通常的位置(我确实看到了上面的草图)。

既然这个提议可以继续推进,我们应该就 API 的细节达成一致。 我目前看到的问题:

  • 要包含哪些功能?
  • 要处理的类型(仅uint64uint32uint64uint32uint16uint8uintptr )?
  • 签名详细信息(例如TrailingZeroesxx(x uintxx) uint ,其中 xx = 64、32 等,而TrailingZeroes(x uint64, size uint) uint其中大小是 64、32 等之一 - 后者会导致更小的 API,并且可能仍然足够快取决于实现)
  • 一些名称自行车脱落(我喜欢“黑客的喜悦”术语,但将其拼写出来可能更合适)
  • 对于这个包,有没有比 math/bits 更好的地方。

@brtzsnr您想继续带头开展这项工作吗? 如果您想在本期中进行初始设计并对其进行迭代,或者您更喜欢设置专门的设计文档,我可以。 或者,我可以拿起它并将其向前推进。 我喜欢在 1.9 阶段看到这一点。

我个人更喜欢拼写出来的 xx 名称: bits.TrailingZeroes(uint64(x), 32) 比 bits.TrailingZeroes32(x), imo 更麻烦且可读性更差,并且该命名方案适用于平台变量大小uintptr 更容易(xx = Ptr?)。

例如,如果您没有在第一个版本中包含 uint8 但后来添加它,它也会更加明显 - 突然间会有很多新的 xx = 8 函数而不是一堆添加 8 的更新的文档注释到有效尺寸列表,并在发布文档中添加了一个很容易遗漏的注释。

如果使用拼写的名称,我认为应该将 Hacker 的高兴术语作为“也称为”包含在描述中,以便搜索(在页面中或通过搜索引擎)如果您搜索错误的拼写,则可以轻松找到规范名称.

我认为 math/bits 是一个不错的地方——它是带有位的数学。

我应该注意到SwapBytes{16,32,64}sync/atomic函数比SwapBytes(..., bitSize uint)更一致。

尽管strconv包确实使用了ParseInt(..., bitSize int)模式,但bitsatomic之间的关系比strconv

我不喜欢 SwapBytes(uint64, size uint) 的三个原因:

  1. 人们可能会在大小不是编译时间常数的情况下使用它(在
    至少对当前编译器而言),
  2. 它需要大量的类型转换。 首先将 uint32 转换为
    uint64,然后将其转换回来。
  3. 我们应该为未来的通用保留通用名称 SwapBytes
    函数的版本(当 Go 获得泛型支持时)。

@griesemer Robert,请接管该提案。 我将有时间在一个月内推进这项提议,但进展不应在此之前停滞。

看起来对表单签名有明显的偏好:

func fffNN(x uintNN) uint

这留下了要支持哪个 NN 的问题。 为了继续讨论,让我们关注 NN=64。 根据需要添加其余类型(包括 uintptr)将是直接的。

这让我们直接回到@brtzsnr的原始提议和以下函数(以及它们对不同类型的相应变体),以及人们在此期间提出的建议:

// LeadingZeros64 returns the number of leading zero bits in x.
// The result is 64 if x == 0.
func LeadingZeros64(x uint64) uint

// TrailingZeros64 returns the number of trailing zero bits in x.
// The result is 64 if x == 0.
func TrailingZeros64(x uint64) uint

// Ones64 returns the number of bits set in x.
func Ones64(x uint64) uint

// RotateLeft64 returns the value of x rotated left by n%64 bits.
func RotateLeft64(x uint64, n uint) uint64

// RotateRight64 returns the value of x rotated right by n%64 bits.
func RotateRight64(x uint64, n uint) uint64

我们也可能有一组 Swap 函数。 就我个人而言,我对这些持怀疑态度:1) 我从来不需要 SwapBits,并且 SwapBytes 的大多数用途是由于代码在不应该时具有字节序。 注释?

// SwapBits64 reverses the order of the bits in x.
func SwapBits64(x uint64) uint64

// SwapBytes64 reverses the order of the bytes in x.
func SwapBytes64(x uint64) uint64

那么可能会有一组整数运算。 它们只会在这个包中,因为它们中的一些(例如 Log2)可能与这个包中的其他函数很接近。 这些功能可以在别处。 也许包名bits需要调整。 注释?

// Log2 returns the integer binary logarithm of x.
// The result is the integer n for which 2^n <= x < 2^(n+1).
// If x == 0, the result is -1.
func Log2(x uint64) int

// Sqrt returns the integer square root of x.
// The result is the value n such that n^2 <= x < (n+1)^2.
func Sqrt(x uint64) uint64

最后, @minux建议了诸如AddUint32 。我暂时不考虑这些操作,因为我认为正确指定它们更棘手(而且种类更多)。 我们可以稍后再与他们联系。 让我们对上述达成一些共识。

问题:

  • 关于函数名的任何意见?
  • 关于整数操作的任何意见?
  • 对包名/位置有什么意见吗?

我更喜欢长函数名称。 Go 避免在函数名称中使用缩写。 对于以这种方式搜索软件包的人,评论可以说出他们的昵称(“nlz”、“ntz”等)。

@griesemer ,您的消息中似乎有错别字。 注释与函数签名不匹配。

我在压缩应用程序中使用SwapBits 。 也就是说,主要架构是否提供任何有效进行位反转的能力? 我打算只用我自己的代码做:

v := bits.SwapBytes64(v)
v = (v&0xaaaaaaaaaaaaaaaa)>>1 | (v&0x5555555555555555)<<1
v = (v&0xcccccccccccccccc)>>2 | (v&0x3333333333333333)<<2
v = (v&0xf0f0f0f0f0f0f0f0)>>4 | (v&0x0f0f0f0f0f0f0f0f)<<4

@bradfitz , @dsnet :更新签名(修正拼写错误)。 还更新了问题(人们更喜欢明确的快捷方式名称)。

除非有对位交换的本地支持,否则我会投票放弃SwapBits 。 无论是仅在每个字节内交换位还是在整个 uint64 中交换位,仅凭名称就可能有点含糊不清。

为什么仅根据指令可用性排除某些内容? 反转位
在 FFT 中有显着的用途。

回答您关于反向位指令可用性的问题:arm 有
RBIT 指令。

@griesemer至于函数的类型签名,如

func Ones64(x uint64) uint
func RotateLeft64(x uint64, n uint) uint64

RotateLeftN等是否需要 uint,因为这对于容易内化(如果可能)是必要的,还是仅仅因为那是域? 如果没有强烈的技术需求,即使负值没有意义,也有更强大的先例来使用整数。 如果有很强的技术需求,它们是否应该是 uintN 为适当的 N 以规律性?

不管OnesN及其同类都应该返回整数,除非这些操作可以与其他位操作组合在一起,在这种情况下,它们应该为适当的 N 返回一个 uintN。

@jimmyfrasche因为它是域。 cpu无所谓。 除了比较之外,对于大多数操作来说,something 是 int 还是 uint 无关紧要。 也就是说,如果我们将它设为 int,我们必须解释它从不为负(结果),或者它不能为负(参数),或者指定当它为负时应该做什么(旋转参数)。 在那种情况下,我们可能只需要一个 Rotate 就可以逃脱了,这会很好。

我不相信使用 int 会使事情变得更容易。 如果设计得当,uint 将流向 uint(数学/大中就是这种情况)并且不需要转换。 但即使需要转换,它在 CPU 成本方面也是免费的(尽管不是在可读性方面)。

但我很高兴听到/看到令人信服的论据。

旋转位计数应该是 uint(没有特定大小)以匹配
语言的移位运算符。

不使用int的原因是会产生歧义
RotateRight -2 位等于 RotateLeft 2 位。

@minuxRotate(x, n)被定义为将x旋转n mod 64位时没有歧义:向左旋转 10 位与向右旋转 64-10 = 54 位相同,或(如果我们允许 int 参数)乘 -10 位(假设正值意味着向左旋转)。 -10 mod 64 = 54。即使使用 CPU 指令,我们也很可能免费获得效果。 例如,32 位 x86 ROL/ROR 指令已经只查看 CL 寄存器的底部 5 位,有效地对 32 位循环执行 mod 32。

唯一真正的论点是与 stdlib 其余部分的规律性。 有很多地方使用 uint 很有意义,但使用 int 来代替。

由于 math/big 是一个显着的例外,我认为这是另一个问题,但这是需要考虑的。

我认为@minux对阅读/调试代码的人

当包的名称已经被称为bits时,bits.SwapBits真的应该用bits后缀命名吗? bit.Swap 怎么样?

另一个想法是 bits.SwapN(v uint64, n int) 而不是 bits.SwapBytes,其中 n 表示交换的分组依据的位数。 当 n=1 时,位被反转,当 n=8 时,字节被交换。 一个好处是 bits.SwapBytes 不再需要存在,尽管我从未见过任何其他 n 值的必要性,也许其他人会不同意。

如上所述LeadingZeros64TrailingZeros64Ones64都是 LGTM。

带有签名轮换金额的单个Rotate64很有吸引力。 大多数旋转也将是一个恒定的量,因此(如果/当这变成一个内在函数时)没有单独的右/左函数不会有运行时成本。

我对位/字节改组/交换/反转没有强烈的看法。 对于反转位, bits.Reverse64似乎是一个更好的名字。 也许bits.ReverseBytes64 ? 我觉得Swap64有点不清楚。 我看到了让Swap64采取组大小的吸引力,但是当组大小不均分 64 时会出现什么行为? 如果唯一重要的组大小是18 ,那么给它们单独的功能会更简单。 我也想知道某种通用位域操作是否会更好,也许可以查看 arm64 和 amd64 BMI2 指令以获得灵感。

我不相信整数数学函数属于这个包。 它们看起来更像是属于数学包,在那里他们可以在实现中使用位函数。 相关,为什么不func Sqrt(x uint64) uint32 (而不是返回uint64 )?

尽管AddUint32和朋友很复杂,但我希望我们最终会重新审视它们。 除此之外, MulUint64将提供对 HMUL 的访问,即使是超极简主义的 RISC-V ISA 也将其作为指令提供。 但是,是的,让我们先来点东西; 我们可以随时扩展。

bits显然是正确的包名。 我很想说导入路径应该只是bits ,而不是math/bits ,但我感觉并不强烈。

bits 显然是正确的包名。 我很想说导入路径应该只是位,而不是数学/位,但我感觉并不强烈。

IMO,如果它只是bits ,(不阅读包文档)我希望它有点类似于包bytes 。 也就是说,除了对位进行操作的函数之外,我还希望找到ReaderWriter用于从字节流读取位和从字节流写入位。 使它成为math/bits使它更明显它只是一组无状态函数。

(我不是建议我们为比特流添加 Reader/Writer)

带有签名旋转量的单个 Rotate64 很有吸引力。 大多数旋转也将是一个恒定的量,因此(如果/当这变成一个内在函数时)没有单独的右/左函数不会有运行时成本。

我可以看到通过将左/右旋转组合成一个Rotate来拥有一个稍微小一点的包 API 是多么方便,但是还有一个问题是有效地记录n参数到表明:

  • n导致左旋转
  • n导致没有变化
  • n导致向右旋转

对我来说,增加的心理和文档开销似乎并不能证明将两者合并为一个Rotate是合理的。 有一个明确的RotateLeftRotateRight其中nuint对我来说感觉更直观。

@mdlayher请记住,对于 n 位字,向左旋转 k 位与向右旋转 nk 位相同。 即使你把它分成两个函数,你仍然需要理解向左旋转 k 位总是意味着旋转 (k mod n) 位(如果旋转超过 n 位,则重新开始)。 一旦你这样做了,你就可以说旋转函数总是旋转 k mod n 位,你就完成了。 没有必要解释负值或第二个函数。 它只是有效。 其实更简单。

最重要的是,硬件(例如在 x86 上)甚至会自动为您执行此操作,即使对于非常量的 k。

@griesemerRotate的缺点是它没有说明哪个方向是积极的。 Rotate32(1, 1)等于 2 还是 0x80000000? 例如,如果我正在阅读使用它的代码,我希望结果是 2,但显然@mdlayher会希望它是 0x80000000。 另一方面, RotateLeftRotateRight是明确的名称,与它们是否采用有符号或无符号参数无关。 (我不同意@minux 的观点,即 RotateRight by -2 是否等于 RotateLeft 2 是不明确的。这些显然与我等价,我不知道你还能如何指定它们。)

@aclements可以通过只有一个名为 RotateLeft64 的函数并使用负值向右旋转来修复。 或者使用文档。 (FWIW,在你的例子中,我也期望 2,而不是 0x800000000。)

@josharian ,我同意,尽管拥有RotateLeft而没有RotateRight似乎有点奇怪。 如果计算旋转,则有符号旋转的一致性似乎具有潜在价值,但对于恒定旋转,我更愿意阅读“向右旋转两位”的代码,而不是“通过开玩笑负两位向左旋转”。

用 docs 解决这个问题的问题是 docs 无助于调用函数的代码的可读性。

编写代码的人想要向左旋转或向右旋转。 他们不想向左或向右旋转,具体取决于。 如果我们只提供rotate-left,那么任何想要向右旋转的人都必须编写一个表达式来将他们的向右旋转转换为向左旋转。 这是一种计算机可以做的很简单但程序员会犯错误的表达方式。 为什么要让程序员自己写呢?

我相信。

@aclements @ianlancetaylor点了。 (也就是说,RotateLeft/Right 的实现可能只会调用rotate 的一个实现。)

一个名字的想法是把类型放在包名而不是签名名中。 bits64.Ones而不是bits.Ones64

@btracey ,我在那里吐了一点。 :) 我们没有flag64.Intmath64.Floatfrombitssort64.Floatsatomic64.StoreInt

@bradfitz golang.org/x/image/math/f64

我没有想到atomic是一个先例(谢天谢地,这不是我正常的 Go 用法)。 其他示例不同,因为包比针对多种不同类型重复的一组函数大。

当您转到bits64并查看时,文档更易于查看(例如,在 godoc 上)
Ones LeadingZeros TrailingZeros

而不是去bits并看到
Ones8 Ones16 Ones32 Ones64 LeadingZeros8 ...

@iand ,这也很恶心。 我想 32 和 64 对 Aff、Mat 和 Vec 的所有现有数字后缀看起来并不好。 不过,我会说这是一个例外,而不是正常的 Go 风格。

在 Go 中有很强的先例使用 pkg.foo64 命名风格而不是 pkg64.foo 。

如果我们有 n 种类型,包 API 确实会变大 n 倍,但 API 复杂性不会。 解决此问题的更好方法可能是通过文档以及如何通过 go doc 等工具呈现 API。 例如,而不是:

// TrailingZeros16 returns the number of trailing zero bits in x.
// The result is 16 if x == 0.
func TrailingZeros16(x uint16) uint

// TrailingZeros32 returns the number of trailing zero bits in x.
// The result is 32 if x == 0.
func TrailingZeros32(x uint32) uint

// TrailingZeros64 returns the number of trailing zero bits in x.
// The result is 64 if x == 0.
func TrailingZeros64(x uint64) uint

这在查看 API 时明显增加了心理开销,我们可以将其表示为:

// TrailingZerosN returns the number of trailing zero bits in a uintN value x for N = 16, 32, 64.
// The result is N if x == 0.
func TrailingZeros16(x uint16) uint
func TrailingZeros32(x uint32) uint
func TrailingZeros64(x uint64) uint

godoc 可能对在这个意义上相似的函数很聪明,并将它们显示为一个带有单个文档字符串的组。 这对其他软件包也很有帮助。

总而言之,我认为提议的命名方案看起来不错并且符合 Go 传统。 演示问题是一个单独的问题,可以在别处讨论。

+1 将位大小放入包名称中。

bits64.Log2 比 bits.Log264 读起来更好(我觉得 Log2 确实属于那里 - 并且包命名不是保留它的好理由)

最后,对于按标量类型“参数化”的每种类型,它是相同的 API,因此如果函数在不同类型之间具有相同的名称,则使用单独的包更容易重构代码 - 只需更改导入路径(全字替换是与 gofmt -r 无关,但自定义后缀转换更尴尬)。

我同意@griesmeyer的观点,即函数名称上的 bitsize 后缀可以是惯用的,但我认为只有在包中存在独立于类型的重要代码时才值得这样做。

我们可以从编码/二进制中窃取一个播放,并创建名为 Uint64、Uint32、...的变量,这些变量将具有接受关联的预定义类型的方法。

bits.Uint64.RightShift
bits.Uint8.Reverse
位.Uintptr.Log2
...

@griesemer它怎么知道如何将它们分组? 在文档中以 N 结尾的函数名而不是实际名称的一个文档字符串,然后在没有文档字符串的函数中找到一个公共前缀来匹配不一致? 这如何概括? 为很少的包提供服务似乎是一个复杂的特殊情况。

@rogpeppe它可能是 bits.Log64 并且文档说它是 base 2(它在 bits 包中还有什么?)虽然像 8/16/32/64/ptr 包一​​样混乱,但它确实在一个级别上使它们各自更清洁。 (而且看起来你的传输在句子中被切断了。)

@nerdatmath这会略有不同,因为它们在口语意义上具有相同的接口,但在 Go 意义上则不同,就像在编码/二进制中的情况一样(它们都实现 binary.ByteOrder),因此变量仅用于命名空间除非类型被导出,否则 godoc 不会选择任何方法 (#7823),这以不同的方式是混乱的。

我对大小后缀没问题,但总而言之,我更喜欢单独的包。 不过,还不足以将其推过这条评论。

@nerdatmath如果它们是 vars,那么它们是可变的(你可以做bits.Uint64 = bits.Uint8 ),这将阻止编译器将它们视为内在函数,这是(至少部分)包的动机之一。

如果变量是未导出的类型并且没有状态(未导出的空结构),则它们并不是真正可变的。 但是如果没有一个通用的接口,godoc(今天)就会很糟糕。 如果那是答案,则可以修复。

但是,如果你们最终使用多个包,如果有足够的重复和数量来保证它,至少可以将包命名为“X/bits/int64s”、“X/bits/ints”、“X/ bits/int32s”以匹配“错误”、“字符串”、“字节”。 好像还是爆了很多包。

我认为我们不应该走多个特定于类型的包的路径,拥有单个包位很简单,并且不会增加 Go 库中包的数量。

我不认为 Ones64 和 TrailingZeroes64 是好名字。 他们不通知他们返回一个计数。 添加前缀 Count 会使名称更长。 在我的程序中,这些函数通常嵌入在较大的表达式中,因此简短的函数名称会增加可读性。 虽然我使用了“Hacker's Delight”一书中的名字,但我建议遵循 Intel 汇编程序助记符:Pocnt64、Tzcnt64 和 Lzcnt64。 名称很短,遵循一致的模式,通知计数已返回并且已经建立。

通常,Go 中的计数以整数形式返回,即使计数永远不会为负数。 主要示例是内置函数 len,但 Read、Write、Printf 等也都返回整数。 我发现使用 uint 值进行计数非常令人惊讶,并建议返回一个 int 值。

如果选择是在(滥用符号)math/bits/int64s.Ones 和 math/bits.Ones64 之间,那么我肯定更喜欢单个包。 在包标识符中包含位比按大小分区更重要。 包标识符中的位使代码更容易阅读,这比包文档中重复的轻微不便更重要。

@ulikunitz在广泛使用位的代码中,您始终可以执行ctz := bits.TrailingZeroes64等操作。 这在具有全局自记录名称和复杂代码的本地紧凑名称之间取得了平衡。 相反的转换countTrailingZeroes := bits.Ctz64不太有用,因为好的全局属性已经丢失。

我很少在位级别弄乱东西。 每次我都必须查看很多这些东西,因为我已经好几年没有考虑它了,所以我发现所有 Hacker's Delight/asm 风格的名称都非常神秘。 我宁愿它只是说它做了什么,这样我就可以找到我需要的那个并重新开始编程。

我同意@ulikunitz的长名字

init() instead of initialize(), 
func instead of function
os instead of operatingsystem
proc instead of process
chan instead of channel

此外,我会挑战 bits.TrailingZeroes64 是自我记录的。 尾随零是什么意思? self 文档属性存在的前提是假设用户对文档的语义有所了解。 如果不使用 Hacker's Delight 来保持一致性,为什么不将其称为 ZeroesRight64 和 ZeroesLeft64 以匹配 RotateRight64 和 RotateLeft64?

澄清一下,我并不是要暗示任何时候使用它时,您应该做的第一件事就是创建一个简短的别名,并且永远不要使用给定的名称。 我的意思是如果你重复使用它,你可以给它取别名,如果这能提高代码的可读性。

例如,如果我正在调用 strings.HasSuffix 我大部分时间都使用给定的名称,因为我会调用它一次或两次,但是,时不时地,在一次性 ETL 脚本或类似的脚本中,我会最终将它称为一堆,所以我会做ends := strings.HasSuffix ,它更短并使代码更清晰。 这很好,因为别名的定义就在眼前。

缩写名称适用于极其常见的事物。 许多非程序员都知道操作系统的含义。 任何阅读代码的人,即使他们不知道 Go,也会明白 func 是函数的缩写。 通道是 Go 的基础并被广泛使用,所以 chan 很好。 位函数永远不会非常常见。

如果您不熟悉这个概念,TrailingZeroes 可能无法准确地告诉您发生了什么,但它可以让您对正在发生的事情有更好的了解,至少大致上,比 Ctz。

@jimmyfrasche关于https://github.com/golang/go/issues/18616#issuecomment -275828661:go/doc 很容易识别同一文件中的连续函数序列,这些函数的名称都以相同的前缀,并有一个后缀,它只是一个数字序列和/或可能是一个相对于前缀“短”的词。 我将它与只有这些函数中的第一个具有文档字符串的事实相结合,而另一个具有匹配前缀的则没有。 我认为这在更一般的环境中可以很好地工作。 也就是说,让我们在别处讨论这个问题,以免劫持这个提议。

仅供参考,创建 #18858 来讨论go/doc变化,并将该对话与此对话分开。

@mdlayher感谢您这样做。

@rogpeppe将大小放在包名中听起来很有趣,但从评论和现有 Go 风格来看,我认为首选是将大小放在函数名中。 关于 Log2,我想说,让我们离开 2。 (另外,如果你还有什么想补充的,请重复一遍,你的评论似乎在句子中间被截断了。)

@ulikunitz我通常是第一个投票给短名字的人; 尤其是在本地上下文中(以及在全局上下文中非常常用的名称)。 但是这里我们有一个包 API,并且(至少根据我的经验)这些功能不会在客户端中普遍出现。 例如,math/big make 使用LeadingZeros、Log2 等,但它只是一两个调用。 我认为更长的描述性名称是可以的,从讨论来看,大多数评论都支持这一点。 如果你经常调用一个函数,把它包装在一个具有短名称的函数中是很便宜的。 额外的调用将被内联掉。 FWIW,英特尔助记符也不是很好,它们的重点是“计数”(cnt),然后剩下2个需要解密的字母。

我同意 Log2 中的 2 不是必需的。 bits.Log表示以 2 为底。

bits.Log64 SGTM。

@griesemer我的截断句可能是笨拙编辑的产物。 我刚刚删除了它。

@griesemer我想知道是否值得继续使用自行车脱落这个名字。 我保持简短:我有两个论点:关于返回数字的简短和清晰。 包 big 在内部使用 nlz,而不是 LeadZeros。 我同意这些功能的使用次数很少。

@ulikunitz我认为这里的大多数反馈都支持不是快捷方式的清晰函数名称。

@ulikunitz ,另外:

包 big 在内部使用 nlz,而不是 LeadZeros。

内部私有未导出名称在这里没有影响。

重要的是公共 API 的一致性和标准感觉。

Bikeshedding 可能很烦人,但当我们永远被他们困住时,名字很重要。

CL https://golang.org/cl/36315提到了这个问题。

我已上传https://go-review.googlesource.com/#/c/36315/作为初始(部分测试)API(具有初步实现)以供审查(代替设计文档)。

我们仍在确保 API 是正确的,所以现在请只对 API (bits.go) 发表评论。 对于小问题(错别字等),请对 CL 发表评论。 对于设计问题,请对此问题发表评论。 我会不断更新 CL,直到我们对界面感到满意为止。

具体来说,不要担心实现。 它是基本的,仅经过部分测试。 一旦我们对 API 感到满意,就可以轻松扩展实现。

@griesemer我确定你很好,但如果它有用,我有一个带有测试的 CLZ 程序集实现https://github.com/ericlagergren/decimal/tree/ba42df4f517084ca27f8017acfaeb69629a090fb/internal/arith你可以随意偷/摆弄/随便。

@ericlagergren感谢您的链接。 一旦 API 稳定下来并且我们在各处都进行了测试,我们就可以开始优化实现。 我会牢记这一点。 谢谢。

@griesemer ,看起来您提议的 API 文档取决于 go/doc 能够充分记录具有相同前缀但整数后缀的函数。

计划是否也会在 1.9 之前进行处理?

18858,就是!

@mdlayher那将是理想的。 但它不会成为一个阻碍,因为它影响“仅”文档,而不是 API(必须保持向后兼容)。

@griesemer @bradfitz我花了一些时间重新思考我对函数名称的立场。 选择名称的一个重要目标必须是使用 API 的代码的可读性

下面的代码确实很容易理解:

n := bits.LeadingZeros64(x)

我对 Ones64(x) 的清晰度还是有点怀疑的,但是它和 LeadZeros 和 TrailingZeros 是一致的。 所以我放弃我的观点并支持当前的提案。 作为一个实验,我使用我现有的代码进行包的实验性纯 Go 实现,包括测试用例。

存储库: https :
文档: https :

我们可能也有一组 Swap 函数 [...] SwapBytes 的大多数用途是由于代码在不应该知道的时候是字节序的。 注释?

出于这个原因,我想将SwapBytes排除在 API 之外。 我担心人们会开始以不可移植的方式使用它,因为它比binary/encoding.BigEndian更简单(或假设更快)。

出于这个原因,我想将 SwapBytes 排除在 API 之外。 我担心人们会开始以不可移植的方式使用它,因为它比 binary/encoding.BigEndian 更简单(或被认为更快)。

@mundaym这些例程会被内化吗? 让SwapBytes64直接编译为BSWAP可能会超过人们不正确地使用它,imo。

我担心人们会开始以不可移植的方式使用它,因为它比二进制/编码更简单(或假设更快)。BigEndian

我相信ReverseBytes的使用只有在与unsafe结合使用时才不可移植,在这种情况下,您可以按照机器在内存中布置数据的方式在低级别访问数据。 但是,以这种方式使用encoding.{Little,Big}Endian.X同样是不可移植的。 我期待ReverseBytes用于压缩,如果不包含它会很伤心。

@ericlagergren

这些惯例会被内化吗?

是的, encoding/binary中执行字节反转的函数使用编译器识别的代码模式,并且在支持未对齐数据的平台上(或可能)优化为单个指令(例如BSWAP )访问(例如 386、amd64、s390x、ppc64le 和其他可能的)。

让 SwapBytes64 直接编译为 BSWAP 可能会超过人们不正确地使用它,imo。

我倾向于不同意。 SwapBytes在不知道字节序的代码中可能有合法用途,但我怀疑它会被误用而不是正确使用。

请注意,runtime/internal/sys 已经优化了 Go、汇编和编译器内部版本的尾随零和字节交换,因此我们可以重用这些实现。

@dsnet有趣,你有什么特别的想法吗?

@aclements你知道在运行时在哪里使用字节交换内部函数吗? 我没有发现测试之外的任何用途。

@mundaym ,AFAIK 它没有在运行时使用。 我们不必对内部 API 如此小心,所以我认为我们只是在添加 ctz 时添加了它,因为它很容易。 :)(Popcount 会是更好的选择。)

全部:只是提醒一下,现在请专注于 API。 CL 中的实现只是一个简单而缓慢的实现,它允许我们实际使用这些函数并对其进行测试。 一旦我们对 API 感到满意,我们就可以调整实现。

关于这一点:我们可能应该包括uintptr版本,因为它不能保证(尽管可能)它与uint32uint64 。 (请注意, uint 始终是uint32uint64 )。 意见? 留到以后? 什么是好名字? ( LeadingZerosPtr ?)。

我们现在应该做 uintptr,否则 math/big 不能使用它。 后缀Ptr SGTM。 我认为我们也应该做uint ,否则所有调用代码都需要围绕 int 大小编写条件代码才能进行免转换调用。 没有命名的想法。

uintptr 的 Ptr,uint 没有后缀。 Ones、Ones8、Ones16、Ones32、Ones64、OnesPtr 等。

我在这里可能是少数,但我反对添加采用 uintptr 的函数。
math/big 应该从一开始就使用 uint32/uint64。 (事实上​​,我
只需对所有架构使用 uint64 并将优化留给
编译器。)

问题是这样的:
math/big 想要使用原生字长来获得最佳性能,
然而,虽然很接近,但 uintptr 并不是正确的选择。
不能保证指针的大小与指针的大小相同
本地字大小。
最好的例子是 amd64p32,我们也可以添加 mips64p32 和
mips64p32le 之后,它在 64 位 MIPS 主机中更受欢迎。

我当然不想鼓励更多这样的用法。 uintptr 是最多的
用于指针,而不是用于架构中性整数。

然而,一种解决方案是在 API 中引入一个类型别名(也许它
应该是品牌类型,这是有待讨论的)。
type Word = uint32 // 或 64 位架构上的 uint64
// 我们可能还需要为 Word 提供一些理想的常量,例如
位数、掩码等。

然后引入没有任何类型前缀的函数来取Word。

事实上,我想如果 math/bit 提供两个结果 add、sub、mul,
div; math/big 可以在没有任何特定于体系结构的程序集的情况下重写。

数学/大出口type Word uintptr 。 我同意这可能是一个错误,但我相信兼容性需要保留。 因此,如果我们想使用 math/bits 来实现 math/big,我认为我们会这样做,我们需要 uintptr API。

有点跑题了,但是原生的无符号字大小不就是 uint 吗? 在这种情况下, uint 没有后缀似乎是正确的。

我不希望单一类型 Word 在不同的体系结构上有所不同。 这似乎为调用者和文档引入了很多复杂性。 坚持现有类型意味着您可以简单地使用它。 拥有一种跨架构而不同的类型意味着围绕该类型设计所有代码,或者在受构建标签保护的文件中拥有一堆适配器。

FWIW,math/big 的实现仍然可以使用基于 uint32/64 的位 API(如果没有 uintptr 版本的函数)。

@minux我不反对 2-result add,sub,mul,div - 但让我们在第二步中做到这一点。 如果我们想要体面的性能,我们仍然需要汇编代码。 但我很高兴被编译器说服了......

我已将 uint 和 uintptr 版本添加到 API 中。 看看 CL 和评论。 不过,我对功能的激增不太满意。

@josharian即使在 64 位机器上,本机 uint 也可能是 32 位值。 没有保证。 我确实意识到 uintptr 也不保证机器字的大小; 但当时这似乎是更明智的选择,如果回想起来是错误的。 也许我们真的需要一个 Word 类型。

如果对 uintptr 函数的唯一合法需求是支持 math/big,那么 math/bits 的实现也许可以放在内部包中:只在 math/big 中使用 uintptr 的东西并公开其余的。

@jimmyfrasche@griesemer这将如何影响 big.Int.Bits? 即它仍然是零分配吗?

uint 也是错误的。 它在 amd64p32 上是 32 位的。
实际上,math/big 可以使用 uint 而不是 uintptr,但是在 Go 1,
int/uint 总是 32 位,这使得 uintptr 成为唯一可能的解决方案
对于数学/big.Word。

我们正在设计一个新的包,所以我认为兼容性不是很大
忧虑。 math/big 使用了错误的类型,但这更像是一个历史
事故。 让我们不要再犯这个错误了。

我不明白为什么使用始终是最多的统一类型“Word”
给定架构的有效整数类型比
使用 uintptr。 对于用户代码,您可以将其视为一种魔术类型,即
32 位或 64 位,取决于架构。 如果你能写
您的代码使用 uintptr(其宽度也取决于体系结构)在
架构独立的方式,那么我相信你可以用 Word 做同样的事情。
Word 在所有架构上都是正确的。

为了 API 的扩散,我建议我们省略 8 个函数
和第一遍中的 16 位类型。 它们很少使用,而且大多数
无论如何,架构只会提供 32 位和 64 位指令。

math/big 定义和导出 Word,但它 a) 与 uintptr 不直接兼容(它是一种不同的类型),并且 b) 正确使用 Word 并且不对它的实现做出假设的代码将能够用于任何类型的 Word 实现. 我们可以改变它。 我不明白为什么它会影响 API 兼容性保证(虽然还没有尝试过)。 无论如何,让我们把它放在一边。 我们可以分开处理。

我们可以简单地做所有明确大小的 uint 类型吗? 如果我们知道 uint/uintptr 的大小是一个常数,我们就可以轻松地编写一个调用大小合适的函数的 if 语句。 if 语句将被常量折叠掉,并且相应的包装函数只是调用将被内联的大小函数。

最后,关于 Word 类型(独立于数学/大)的正确解决方案是什么? 当然,我们可以用数学/位来定义它,但那是正确的地方吗? 它是特定于平台的类型。 它应该是预先声明的类型 'word' 吗? 那么为何不?

这里有任何直接提到 uintptr 的函数签名(或名称)似乎是一个错误。 人们不应该在 Go 指针上做这种小事。

如果应该有自然机器字类型的函数,我认为现在可以参考 int/uint 。 我们没有在 math/big 中使用 uint,因为它在 64 位系统上是 32 位的,但我们已经解决了这个问题。 虽然 math/big 本身就是一个连贯的世界,在那里拥有自己的类型 big.Word 是有意义的,但这个包更像是一个挑选和选择实用程序的集合。 在这种情况下定义新类型的吸引力不大。

我根本不知道我们是否需要 int/uint 变体。 如果通常需要它们,在这个包中定义它们比强制所有调用者编写 if 语句更有意义。 但我不知道它们是否会被普遍需要。

为了解决与 math/big 的潜在不匹配问题,至少有两种解决方案不涉及在此处的 API 中提及 uintptr。

  1. 在 math/big 中定义文件 word32.go 和 word64.go,并使用构建标记和 uintptr 接受包装器进行适当重定向(并确保编译器内联执行正确的操作)。

  2. 将 big.Word 的定义更改为 uint。 确切的定义是内部实现细节,即使它在 API 中公开。 除了 amd64p32 之外,更改它不会破坏任何代码,即使在那里它似乎也不太可能在 math/big 之外发生。

@rsc :“我们没有在 math/big 中使用 uint,因为它在 64 位系统上是 32 位的,但我们已经修复了它。”是的,我完全忘记了选择 uintptr 的原因。 Go uint/int 类型的全部意义在于具有反映机器自然寄存器大小的整数类型。 我将尝试将 big.Word 更改为 uint,然后看看会发生什么。

全部:更新https://go-review.googlesource.com/#/c/36315/以根据上述讨论排除 uintptr 函数版本。

我暂时更新了 math/big 以使用 math/bits 来查看效果 (https://go-review.googlesource.com/36328)。

几点意见:
1)我倾向于使Leading / TrailingZeros函数的结果uint而不是int 。 这些结果往往用于相应地移动一个值; math/big证明了这一点。 我也赞成让Ones返回uint ,以保持一致性。 相反的论点?

2)我不喜欢One这个名字,但与Leading / TrailingZerosPopulation怎么样?

3) 有些人抱怨Log不适合这个包。 Log恰好相当于 msb 的索引(x == 0 为 -1)。 所以我们可以称它为MsbIndex (虽然 Log 看起来更好)。 或者,我们可以有一个Len函数,它是以位为单位的 x 的长度; 即,`Log(x) == Len(x)-1)。

注释?

我不喜欢 One 这个名字,但与 Lead/TrailingZeros 一致。 人口呢?

为什么不是传统的Popcount

或者,我们可以有一个 Len 函数,它是以位为单位的 x 的长度; 即,`Log(x) == Len(x)-1)。

LenLength听起来是个好名字和好主意。

1.

我不喜欢 One 这个名字,但与 Lead/
尾随零。 人口呢?

位。计数?

@aclements算什么?

由于历史原因,Population 可能是更好的名称,但如果您不熟悉这个术语并且遇到同样的问题,那么它实际上并没有比 Ones 多,只是 Count:Population of what?

即使在包中,它也有点单调,但也许 CountOnes 可以给它一个清晰、明显的名称。

@aclements算什么?

在一个名为“位”的包中,我不确定除了设置位之外你还能计算什么,但 CountOnes 也很好,显然更明确。 添加“Ones”也与LeadingZeros / TrailingZeros的“Zeros”平行。

这是最明显的解释,但仍有歧义。 它可能是计算未设置的位或总位(最后一种解释极不可能,但对于使用不熟悉所涉及概念的位阅读代码并且可能认为无后缀计数就像 unsafe.SizeOf 的人来说,这仍然是一个潜在的陷阱)

也许像 AllOnes 或 TotalOnes 之类的东西来反映 Trailing/LeadingZeroes,但要指定,与那些不同的是,不考虑位置。

AllOnes 听起来像是返回所有的(可能在位掩码中?),或者可能是一个全是的词。

CountOnes 和 TotalOnes 看起来差不多,但由于这个操作最广为人知的名称是“人口计数”,CountOnes 似乎更可取。

我上传了一个新版本 (https://go-review.googlesource.com/#/c/36315/),其中有一些更改:

1) 我将Ones重命名PopCount :大多数人对一致但有点单调的名称Ones并不太兴奋。 每个打算使用这个包的人都会确切地知道PopCount作用:它是这个函数众所周知的名字。 它与其他部分略有不一致,但它的含义变得更加清晰。 我将与拉尔夫沃尔多爱默生一起讨论这个问题(“愚蠢的一致性是小头脑的大妖精......”)。

2)我将Log重命名Lenbits.Len 。 数字 x 的位长是用二进制表示法 (https://en.wikipedia.org/wiki/Bit-length) 表示 x 所需的位数。 看起来很合适,并且不需要Log在这里具有非“有点繁琐”的质量。 我相信这也是一个胜利,因为Len(x) == Log(x) + 1考虑到我们如何定义Log 。 此外,它的优点是现在的结果总是 >= 0,并且它在(琐碎的)实现中删除了一些 +/-1 修正。

总的来说,我现在对这个 API 非常满意(我们可能想稍后添加更多功能)。 我认为我们可能需要认真考虑的另一件事是是否所有结果都应该未签名。 正如我之前指出的,尾随/前导零函数结果往往是必须无符号的移位操作的输入。 那只剩下 Len 和 PopCount 可以说也可以返回无符号值。

注释?

我在 math/big 方面的经验让我很沮丧,因为当我不换班时被函数强制进入 uint 模式。 在 math/big/prime.go 中,我写道

for i := int(s.bitLen()); i >= 0; i-- {

即使 s.bitLen() 返回 int 而不是 uint,因为我不确定没有看,而且我也不确定未来的某些 CL 可能不会将它更改为返回 uint,从而将 for 循环变成无限循环。 需要采取这种防御措施表明存在问题。

Uint 比 int 更容易出错,这就是我们在大多数 API 中不鼓励使用它们的原因。 我认为如果 math/bits 中的每个函数都返回 int 并将转换为 uint 的转换留在 shift 表达式中会更好,这在大多数 shift 表达式中都是必需的。

(我不是提议改变,但我认为现在回想起来,需要 uint 的转变可能是一个错误。也许我们可以在某个时候修复它。如果它不伤害我们的其他 API 那就太好了。)

我支持在 grepping 我的代码以调用尾随零和 popcount 函数后返回 int 。 大多数调用都是在 int 类型的简短变量声明和比较中。 轮班调用当然需要 uint 类型,但非常罕见。

上传小调整。 每 +1 和评论让我们将计数结果保留为int

https://go-review.googlesource.com/36315

我将RotateLeft/Right的输入计数保留为uint否则我们将需要指定当值为负时会发生什么或不允许它。

最后,我们甚至可能会因为LenN(x) == N - LeadingZerosN(x)放弃Len LenN(x) == N - LeadingZerosN(x) 。 意见?

如果此时不再有重要反馈,我建议我们继续使用此 API,并在审核后提交初始实现。 之后,我们可以开始调整实现。

下一步,我们可能想讨论是否以及我们可能想包括哪些其他函数,特别是Add / Sub / 等,它们消耗 2 个参数和进位,并产生结果和进位.

@gri对 10 Len函数的想法? 它只是((N - clz(x) + 1) * 1233) >> 12

它不像 base 2 那样“有点hacky”,但仍然有用。
2017 年 2 月 10 日星期五下午 5:03 Robert Griesemer通知@ github.com
写道:

上传小调整。 每 +1 和评论让我们留下结果
算作整数。

https://go-review.googlesource.com/36315

我将 RotateLeft/Right 的输入计数保留为 uint 否则我们将
需要指定当值为负时会发生什么或不允许它。

最后,由于 LenN(x) == N,我们甚至可能会忽略 Len -
前导零N(x)。 意见?

如果此时没有重要的反馈,我建议我们
继续使用此 API 并在之后提交初始实现
审查。 之后,我们可以开始调整实现。

下一步我们可能想讨论我们是否可以以及哪些其他功能
想要包括,特别是 Add/Sub/ 等消耗 2 个参数和
进位,并产生一个结果和进位。


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/18616#issuecomment-279107013或静音
线程
https://github.com/notifications/unsubscribe-auth/AFnwZ_QMhMtBZ_mzQAb7XZDucXrpliSYks5rbQjCgaJpZM4Lg5zU
.

还。 如果我超出了这个问题的范围,请原谅我,但我赞成检查算术。 例如AddN(x, y T) (sum T, overflow bool)

2017 年 2 月 10 日星期五晚上 8:16,Eric Lagergren通知@ github.com
写道:

还。 如果我超出了这个问题的范围,请原谅我,但我会在
赞成检查算术。 例如 AddN(x, y T) (sum T, overflow bool)

你在谈论签名溢出吗? 或无符号溢出(进位/借位)?

另外,我更喜欢将溢出/进位/借用作为 T 类型返回,以简化
多精度算法。

@ericlagergren (基数为 2) Len函数虽然几乎是 log2,但本质上仍然是一个位计数函数。 一个基数为 10 的Len函数实际上是一个日志函数。 不可否认它很有用,但它似乎不太适合这个包。

是的,我认为加入checked算术会很好。我只是想先关闭当前的 API,这样我们就可以取得进展,而不是来回走动。 到目前为止,大多数评论的人似乎对此感到满意。

关于 Add、Sub 函数:我同意@minux 的观点,即进位应该与参数的类型相同。 此外,我不相信除了这些函数的 uint 参数之外我们还需要其他任何东西:关键是(在大多数情况下)uint 具有平台的自然寄存器大小,这是这些操作最有意义的级别。

数学/检查包可能是那些更好的家。 checked.Addbits.Add更清晰。

@minux我在想签名检查算术,所以溢出。

@griesemer 回复:基数 10 Len :有道理!

如果您愿意,我可以提交 CL 以进行检查算术。 我喜欢@jimmyfrasche将它放在一个单独的包名下的想法。

Add/sub/mul 可能属于也可能不属于bits ,但检查数学并不是这些操作的唯一用途。 更一般地说,我会说这些是“宽”算术运算。 对于加/减,一位进位/溢出结果和一个布尔溢出指示之间几乎没有区别,但我们可能还希望提供加进位和减法借位以使这些可链接。 对于宽 mul,附加结果中的信息远多于溢出。

记住检查/宽算术运算是个好主意,但让我们将它们留给第二轮。

“每个要使用这个包的人都会确切地知道 PopCount 是做什么的”

我以前使用过该功能,但不知何故我不熟悉 PopCount 名称(尽管我应该这样做,因为我确定我从使用“pop”的 Hacker's Delight 中删除了实现)。

我知道我要参加聚会,但“OnesCount”对我来说似乎要明显得多,如果文档评论中提到了“PopCount”这个词,那么寻找它的人无论如何都会找到它。

与检查/宽算术相关:#6815。 但是,是的,让我们开始吧!

@griesemer写道:

(base 2) Len 函数虽然几乎是 log2,但本质上仍然是一个位计数函数。 一个基数为 10 的 Len 函数实际上是一个对数函数。 不可否认它很有用,但它似乎不太适合这个包。

去年 10 月,我写了一个基准来比较“十进制数字长度”问题的几种方法,我在gist 中发布了

作为提案被接受; 可能会有一些小的调整向前推进,这很好。

@rogpeppe :将PopCount更改OnesCount因为它也获得了 4 个赞(就像我建议使用PopCount )。 在文档字符串中称为“人口计数”。

根据@rsc

同样每个@rsc ,所有计数函数都返回int值,并且为了便于使用(并注意 #19113),使用int值进行旋转计数。

LenN函数即使它们只是N - LeadingZerosN 。 但是RotateLeft / Right也存在类似的对称性,我们两者都有。

TrailingZeroes添加了稍微快一点的实现并完成了测试。

在这一点上,我相信我们有一个可用的第一个实现。 请查看https://go-review.googlesource.com/36315 上的代码,尤其是 API。 如果我们都高兴,我喜欢提交这个。

下一步:

  • 更快的实施(主要)
  • 添加附加功能(次要)

我们正在设计一个新的包装

@minux你的意思是新数学/大? 是否可以在某处遵循该过程?

@TuomLarsen@minux用“新包”引用数学/位。 他提到 math/big 是使用 math/bits 的一个例子。 (将来,请更具体地提供您的评论,这样我们就不必搜索和猜测您指的是什么 - 谢谢)。

CL https://golang.org/cl/37140提到了这个问题。

Go 1.9 的数学/位是否会发生任何编译器辅助的内在化?

@cespare取决于我们是否得到它(@khr?)。 除此之外,我们希望有一个像样的平台独立实现。 (我们不想将 math/big 完全转移到使用 math/bits 的原因之一是目前我们在 math/big 中有一些特定于平台的程序集,它更快。)

在我的盘子里,至少对于拱门我们已经做了内在函数
(386、amd64、arm、arm64、s390x、mips,可能是 ppc64)。

2017 年 2 月 17 日星期五下午 12:54,Robert Griesemer < [email protected]

写道:

@cespare https://github.com/cespare取决于我们是否得到它(
@khr https://github.com/khr ?)。 除此之外,我们希望有一个
体面的独立于平台的实现。 (我们不这样做的原因之一
想要将数学/大完全转移到使用数学/位是目前我们
在 math/big 中有一些特定于平台的程序集,速度更快。)


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/golang/go/issues/18616#issuecomment-280763679或静音
线程
https://github.com/notifications/unsubscribe-auth/AGkgIIb8v1X5Cr-ljDgf8tQtT4Dg2MGiks5rdgkegaJpZM4Lg5zU
.

这篇关于在 x86-64 上实现人口计数的最快方法的文章可能会有所帮助:手工编码程序集在速度和简单性方面击败了内在函数 – Dan Luu,2014 年 10 月

CL https://golang.org/cl/38155提到了这个问题。

CL https://golang.org/cl/38166提到了这个问题。

CL https://golang.org/cl/38311提到了这个问题。

CL https://golang.org/cl/38320提到了这个问题。

CL https://golang.org/cl/38323提到了这个问题。

更多讨论:

我:

math/bits.RotateLeft 当前定义为如果其参数小于零则恐慌。
如果它的 arg 小于零,我想改变它以定义 RotateLeft 进行右旋转。

拥有像这样一个恐慌的基本例程似乎不必要地苛刻。 我认为负数的旋转更类似于比字长(它不会恐慌)大的移位,而不是除以零(它会恐慌)。 除以零真的必须恐慌,因为没有合理的结果你会返回。 按负数旋转具有我们可以返回的完美定义的结果。

我认为我们应该将 RotateLeft 和 RotateRight 作为单独的函数保留,即使现在可以与另一个一起实现。 标准用法仍将使用非负参数。

如果我们要在这里做点什么,我们应该在冻结前完成。 一旦 go 1.9 出来,我们就无法改变主意。

抢:

如果你真的想要这样的东西,我只需要一个函数,旋转,它对左取正值,对右取负值。

我:

问题是
位.旋转(x,-5)

阅读这段代码时,我们不清楚我们最终是向左还是向右旋转。
bits.RotateRight(5)
更清晰,即使这意味着数学/位中有两倍的 Rotate* 函数。

迈克尔·琼斯:

带符号的旋转意味着代码中的跳转,是吗? 好像很可惜。

我:

不,使用建议的语义,实现实际上更简单。 屏蔽低 6 位(用于 64 位旋转),它就可以工作。

我更喜欢带有带符号参数的单个 Rotate,因为它意味着函数数量的一半,并且可以在所有情况下非常有效地完成,而不会出现恐慌甚至条件分支。

无论如何,Rotate 是一个专业功能,因此使用它的人肯定会被要求记住正论点向左移动而否定论点向右移动。

您始终可以拆分差异并仅提供带有签名参数的RotateLeft 。 这为方向提供了方便的助记符,但避免了重复功能。

@bcmills @robpike也可以从https://github.com/golang/go/issues/18616#issuecomment -275598092 开始对这个确切主题的先前讨论,并继续发表一些评论

@josharian我已经看到了评论,但仍然更喜欢我的版本。 这可以永远被抛弃,但我正在努力实现简单的实现、简单的定义、简单的理解和简单的记录。 我相信带有带符号参数的单个 Rotate 函数可以满足所有这些,除了定义符号的需要,但是对于能够使用旋转指令的任何人来说,左为正应该是直观的。

但是对于能够使用旋转指令的任何人来说,左边是肯定的应该是直观的。

我喜欢认为自己能够使用旋转指令,我的直觉是“天哪,为什么不说明它是什么方向?它可能已经离开了,但我必须查看文档才能确定。” 我同意这是直观的,如果你要选择一个积极的方向,它会向左旋转,但有一个更高的阈值,如此明显的正确答案,在每个呼叫站点都很清楚你旋转的方向,而无需说它。

就可读性而言,类似于time.Duration API 的内容如何:

const RotateRight = -1

bits.Rotate(x, 5 * RotateRight)

也许是由位定义的常量,或者是读者的练习(调用站点)?

@aclements因此,您最终会得到两个(乘以 N 种类型)具有相同功能的函数,它们唯一的区别是参数中的一个符号。 现在我们对 Add 和 Sub 容忍它,但这是我能想到的唯一例子。

在数轴上,正数向右增加,因此我希望通过符号定义的方向旋转/移位将正数的位向右移动。

如果它与[记录]相反,我没有问题,但我不会称之为直觉。

@cznic位是从右到左写的

我也赞成Rotate (https://github.com/golang/go/issues/18616#issuecomment-275016583) 如果我们让它在两个方向都起作用。

作为对@aclements关注方向的RotateLeft也可以在向右旋转时也可以提供错误的安全感:“它说RotateLeft所以它肯定不会旋转对!”。 换句话说,如果它说RotateLeft最好不要做任何其他事情。

此外, bits.Rotate的使用实际上仅在专业代码中使用。 这不是一个被大量程序员使用的函数。 真正需要它的用户会理解旋转的对称性。

@nathany

位是从右到左写的

位只是二进制数字。 任何基数中的数字都是从左到右书写的——即使在大多数(如果不是全部)从右到左的书写系统中也是如此。 123是一百二十三,不是三百二十一。

数字的被乘数的幂向右减小是另一回事。

再说一遍:我不在乎方向,只是直觉的那个是个人想象的问题。

我喜欢旋转。 在我看来,最低有效位直观地为 0 就足够了。

请同时保留 RotateLeft 和 RotateRight,而不是做一半开发人员会记错的事情。 不过,处理负数似乎很好。

99% 的开发人员永远不会编写旋转指令,因此对明确方向的需求充其量是微弱的。 一个电话就够了。

重新唤醒这个讨论的问题是,两者都需要争论负值是否可以,如果不是,如何处理它们。 由于只有一个,整个论点都消失了。 这是更清洁的设计。

我有点同情关于干净设计的论点,但为了实现它,您必须从“RotateRight”中删除“Right”,同时保持相同的实现,这似乎很奇怪。 实际上,它似乎回答问题的唯一方法是通过名称提出的问题迫使看到它的人阅读文档。
最后,这是一个明确的方向与负值的明确行为的问题。 在常见情况下,负值可能不太受关注。

我要说的是,Rotate 向所有人提出了一个问题,通过文档间接回答了这个问题。
RotateRight 向极少数能够(并且应该)阅读文档的人提出了一个问题,如果他们关心的话。

另一方面, Rotate 可能会阻止人们编写if n < 0 { RotateLeft(...) } else { RotateRight(...) }

@golang/proposal-review 讨论了这一点,最终只有一个函数,但将其命名为RotateLeft ,而不仅仅是Rotate ,以便在调用站点上更加清晰。 负数向右旋转,文档将说明这一点。

CL https://golang.org/cl/40394提到了这个问题。

CL https://golang.org/cl/41630提到了这个问题。

最初的提议加上一些额外的功能在这一点上已经被设计和实现。 随着时间的推移,我们可能会添加到这个库中,但现在它似乎相当“完整”。

最值得注意的是,我们尚未决定或实​​施以下功能:

  • 测试add/mul/etc是否溢出
  • 提供实现 add/mul/etc 的函数,这些函数接受传入的进位并产生结果加进位(或更高的字)

我个人不相信那些属于“位”包(也许测试确实如此)。 实现多精度 add/sub/mul 的函数将允许一些数学/大内核的纯 Go 实现,但我不认为粒度是正确的:我们想要的是优化的内核处理向量,以及最大这些内核的性能。 我不相信我们可以通过仅依赖于 add/sub/mul “intrinsics”的 Go 代码来实现这一点。

因此,除非有重大反对意见,否则现在我想以“完成”来结束这个问题。 如果你反对关闭这个,请在​​下周左右说出来。

我赞成在这些方面添加功能。

我坚信它们属于它们自己的包,如果没有其他原因,只是给它一个更好地反映它们集体功能的名称。

:+1: 关于结束这个问题和 :heart: 到目前为止所做的工作。

由于没有异议,因此关闭。

这是对有关 API 的未来决策的评论,我了解这个特定的设置。

Rotate是一个专业函数; LTR 或 RTL 仅与上下文相关。 @aclements提出了一个有效的问题,而不是一个有效的延伸点。 他的问题可以回答为“这是 RTL,就像整数递增一样”; 很简单,对吧?

但取而代之的是聪明。

“专家职能”的意思是这么简单的事情,很可能很快就被驳回了。 给定一个代码示例,人们可能甚至在遇到代码行之前就已经了解要发生的旋转和方向。 这样的代码通常已经在说明性的 ascii 文档之前。

精神上的动荡不是 Go 可以简单地选择 RTL 作为从 API 角度解释位的标准方式,而是,我首先提取了 1.9 的更改并找到了一个没有对应物的 RotateLeft 并且文档给出了一个示例负步。 这是一个令人麻木的委员会式决定,非常不幸在 1.9 中落地。

我只恳求坚持未来的使用环境。 所有这些都应该是不言而喻的,比如“为什么我们不提供 RotateLeft 的对应物,为什么我们对负步幅感到恐慌或争论 int 与 uint 的步幅”; 最终,因为我认为“专业功能”的意思很容易因为不聪明而被轻易驳回。

让我们在 API 的正当化中避免聪明。 它显示在此 1.9 更新中。

更改https://golang.org/cl/90835提到这个问题: cmd/compile: arm64 intrinsics for math/bits.OnesCount

此页面是否有帮助?
0 / 5 - 0 等级