访问内存的底层数组<T>
Accessing the underlying array of a Memory<T>
在我的应用程序中,我需要遍历文件的内容以生成文件的固定大小块的哈希值。最终目标是实现 Amazon Glacier 的 Tree Hash 算法,我几乎一字不差地从他们的文档中复制了代码。
当我通过 SonarQube 运行 以下代码时,问题发生了:
byte[] buff = new byte[Mio];
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(buff, 0, Mio)) > 0) {
// Process the bytes read
}
我在 while
循环的线路上遇到 Roslyn 问题。问题是“更改 'ReadAsync' 方法调用以使用 'Stream.ReadAsync(Memory, CancellationToken)' 重载”。根据描述,这是因为使用 Memory class 的方法比使用基本数组的方法效率更高。
当 class 可以从头到尾使用时,这可能是正确的。问题是,我需要将数据提供给 HashAlgorithm
的 ComputeHash
方法,并且它们没有任何覆盖接受 Memory
。这意味着我必须使用 Memory
的 ToArray
方法,它会生成数据的 copy。这对我来说听起来效率不高。
我知道可以通过将现有数组传递给其构造函数来创建 Memory
实例,如下所示:
byte[] buff = new byte[Mio];
Memory<byte> memory = new Memory<byte>(buff);
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(memory)) > 0) {
// Use `buff` to access the bytes
}
但是文档不清楚传递给构造函数的数组是否实际用作 Memory
实例的底层存储。
因此,这是我的问题:
- 如何直接将数据从
Memory
提供给 HashAlgorithm
实例?我说的是派生自 HashAlgorithm
的 any 实例,而不是 SHA256 算法。与 Glacier 不同,我的实现不限于 SHA256。
- 存储在
Memory
实例中的数据是否也可以在用于创建它的数组中访问?
- 是否有另一种方法可以访问
Memory
实例中作为数组存储的数据,无需复制?
- 否则,我如何解决 SonarQube 中的外部问题(本例中为 Roslyn 警告)?我没有下拉菜单来更改其状态,就像正常的声纳问题一样。
EDIT 添加有关代码工作原理的附加信息:
它是 AWS's example of computing a Glacier Tree Hash 的第一部分,计算文件中 1Mio 块的第一个哈希值的部分。
这些是上面 while
循环的内容:
// Constructor of the class
// The class implements IDisposable to properly dispose of the Algorithm field
// Constructor is called like this
// `using TreeHash treeHash = new TreeHash(System.Security.Cryptography.SHA512.Create());`
public TreeHash(HashAlgorithm algo) {
this.Algorithm = algo;
}
// Chunk hash generation loop
// first part of the tree hash algorithm
byte[][] chunkHashes = new byte[numChunks][];
byte[] buff = new byte[Mio];
int bytesRead;
int idx = 0;
while ((bytesRead = await inputStream.ReadAsync(buff, 0, Mio)) > 0) {
chunkHashes[idx++] = this.ComputeHash(buff, bytesRead);
}
// Quick wrapper around the hash algorithm
// Also used by the second part of the tree hash computation
private byte[] ComputeHash(byte[] data, int count) => this.Algorithm.ComputeHash(data, 0, count);
我默认使用散列算法的无前缀版本,但我可以切换到托管版本。如果需要,该方法可以变为非 async
.
以下应该有效。它利用 MemoryPool<byte>
得到一个 IMemoryOwner<byte>
,我们可以用它来检索我们的暂存缓冲区。我们需要一个 Memory<byte>
传递给 ReadAsync
调用,所以我们传递 IMemoryOwner<byte>
.
的 Memory
属性
然后我们重组代码以使用 HashAlgorithm.TryComputeHash
方法,该方法接受 ReadOnlySpan<byte>
作为源和 Span<byte>
作为目标。我们做分配一个新数组(而不是使用ArrayPool
)因为你是keeping/storing数组。
byte[][] chunkHashes = new byte[numChunks][];
using var memory = MemoryPool<byte>.Shared.Rent(Mio);
int bytesRead;
int idx = 0;
while ((bytesRead = await inputStream.ReadAsync(memory.Memory, CancellationToken.None)) > 0)
{
var tempBuff = new byte[(int)Math.Ceiling(this.Algorithm.HashSize/8.0)];
if (this.Algorithm.TryComputeHash(memory.Memory.Span[..bytesRead] /*1*/, tempBuff, out var hashWritten))
{
chunkHashes[idx++] = hashWritten == tempBuff.Length ? tempBuff : tempBuff[..hashWritten] /*2*/;
}
else
throw new Exception("buffer not big enough");
}
对于source,我们传Span
property of the Memory<bytes>
buffer, which is again retrieved from the IMemoryOwner<byte>.Memory
property. We slice it to the appropriate length based on the number of bytes read. The Span<byte>
that we pass as the destination must be at least the size of the algorithm's HashSize
属性,也就是bits的个数(not 字节)哈希所需的。由于实现可能(尽管我认为不太可能)使用 不是 8 的倍数的大小,我们 ceiling 将除法四舍五入如有必要。我们不需要调用 AsSpan
因为存在从 T[]
.
的隐式转换
我相信*最终写入的字节数将始终与 HashSize
的长度相同。 If/when 是的,我们只是简单地利用了原始数组。否则我们需要根据写入的哈希字节数将其切片到正确的长度。
如果缓冲区不够大,TryComputeHash
returns false
我们会抛出异常。我非常肯定这不会发生在我们身上,因为我们是根据 HashSize
显式计算大小,但无论如何我们都会将这种情况作为最佳实践来处理。
我已经通过了 CancellationToken.None
,但您可以提供自己的令牌。我还使用 Range
语法而不是显式调用 Slice
。如果这对您不可用或者您只是不喜欢它的外观,您可以明确说明:
/*1*/ memory.Memory.Span.Slice(0, bytesRead)
/*2*/ tempBuff.AsSpan(0, hashWritten).ToArray()
我们可以做出的一些可能假设:
- 假设
HashSize
总是8的倍数
- 假设
HashSize
始终等于写入的字节数并且不对最终数组进行切片
- 假设我们总是提供足够大的缓冲区(按照上面的说法,这就是所需的确切大小)并删除
if
和 Exception
while ((bytesRead = await inputStream.ReadAsync(memory.Memory, CancellationToken.None)) > 0)
{
var tempBuff = new byte[this.Algorithm.HashSize/8];
_ = this.Algorithm.TryComputeHash(memory.Memory.Span[..bytesRead], tempBuff, out _);
chunkHashes[idx++] = tempBuff;
}
* 不幸的是,我不能 100% 肯定地说这些都是有效的假设。我看过的大多数实现的源代码都有一个 Debug.Assert
验证缓冲区大小和写入的字节数是相同的,所以我认为它们是 合理的 。也就是说,我想我个人会坚持使用更详细的选项。
您还会注意到我已经删除了您的 ComputeHash
功能。这并不是说您仍然不能使用它,但我将把它转换为这种基于 Try
的 Memory<>
模式作为对 reader.
的练习
在我的应用程序中,我需要遍历文件的内容以生成文件的固定大小块的哈希值。最终目标是实现 Amazon Glacier 的 Tree Hash 算法,我几乎一字不差地从他们的文档中复制了代码。
当我通过 SonarQube 运行 以下代码时,问题发生了:
byte[] buff = new byte[Mio];
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(buff, 0, Mio)) > 0) {
// Process the bytes read
}
我在 while
循环的线路上遇到 Roslyn 问题。问题是“更改 'ReadAsync' 方法调用以使用 'Stream.ReadAsync(Memory, CancellationToken)' 重载”。根据描述,这是因为使用 Memory class 的方法比使用基本数组的方法效率更高。
当 class 可以从头到尾使用时,这可能是正确的。问题是,我需要将数据提供给 HashAlgorithm
的 ComputeHash
方法,并且它们没有任何覆盖接受 Memory
。这意味着我必须使用 Memory
的 ToArray
方法,它会生成数据的 copy。这对我来说听起来效率不高。
我知道可以通过将现有数组传递给其构造函数来创建 Memory
实例,如下所示:
byte[] buff = new byte[Mio];
Memory<byte> memory = new Memory<byte>(buff);
int bytesRead;
while ((bytesRead = await inputStream.ReadAsync(memory)) > 0) {
// Use `buff` to access the bytes
}
但是文档不清楚传递给构造函数的数组是否实际用作 Memory
实例的底层存储。
因此,这是我的问题:
- 如何直接将数据从
Memory
提供给HashAlgorithm
实例?我说的是派生自HashAlgorithm
的 any 实例,而不是 SHA256 算法。与 Glacier 不同,我的实现不限于 SHA256。 - 存储在
Memory
实例中的数据是否也可以在用于创建它的数组中访问? - 是否有另一种方法可以访问
Memory
实例中作为数组存储的数据,无需复制? - 否则,我如何解决 SonarQube 中的外部问题(本例中为 Roslyn 警告)?我没有下拉菜单来更改其状态,就像正常的声纳问题一样。
EDIT 添加有关代码工作原理的附加信息: 它是 AWS's example of computing a Glacier Tree Hash 的第一部分,计算文件中 1Mio 块的第一个哈希值的部分。
这些是上面 while
循环的内容:
// Constructor of the class
// The class implements IDisposable to properly dispose of the Algorithm field
// Constructor is called like this
// `using TreeHash treeHash = new TreeHash(System.Security.Cryptography.SHA512.Create());`
public TreeHash(HashAlgorithm algo) {
this.Algorithm = algo;
}
// Chunk hash generation loop
// first part of the tree hash algorithm
byte[][] chunkHashes = new byte[numChunks][];
byte[] buff = new byte[Mio];
int bytesRead;
int idx = 0;
while ((bytesRead = await inputStream.ReadAsync(buff, 0, Mio)) > 0) {
chunkHashes[idx++] = this.ComputeHash(buff, bytesRead);
}
// Quick wrapper around the hash algorithm
// Also used by the second part of the tree hash computation
private byte[] ComputeHash(byte[] data, int count) => this.Algorithm.ComputeHash(data, 0, count);
我默认使用散列算法的无前缀版本,但我可以切换到托管版本。如果需要,该方法可以变为非 async
.
以下应该有效。它利用 MemoryPool<byte>
得到一个 IMemoryOwner<byte>
,我们可以用它来检索我们的暂存缓冲区。我们需要一个 Memory<byte>
传递给 ReadAsync
调用,所以我们传递 IMemoryOwner<byte>
.
Memory
属性
然后我们重组代码以使用 HashAlgorithm.TryComputeHash
方法,该方法接受 ReadOnlySpan<byte>
作为源和 Span<byte>
作为目标。我们做分配一个新数组(而不是使用ArrayPool
)因为你是keeping/storing数组。
byte[][] chunkHashes = new byte[numChunks][];
using var memory = MemoryPool<byte>.Shared.Rent(Mio);
int bytesRead;
int idx = 0;
while ((bytesRead = await inputStream.ReadAsync(memory.Memory, CancellationToken.None)) > 0)
{
var tempBuff = new byte[(int)Math.Ceiling(this.Algorithm.HashSize/8.0)];
if (this.Algorithm.TryComputeHash(memory.Memory.Span[..bytesRead] /*1*/, tempBuff, out var hashWritten))
{
chunkHashes[idx++] = hashWritten == tempBuff.Length ? tempBuff : tempBuff[..hashWritten] /*2*/;
}
else
throw new Exception("buffer not big enough");
}
对于source,我们传Span
property of the Memory<bytes>
buffer, which is again retrieved from the IMemoryOwner<byte>.Memory
property. We slice it to the appropriate length based on the number of bytes read. The Span<byte>
that we pass as the destination must be at least the size of the algorithm's HashSize
属性,也就是bits的个数(not 字节)哈希所需的。由于实现可能(尽管我认为不太可能)使用 不是 8 的倍数的大小,我们 ceiling 将除法四舍五入如有必要。我们不需要调用 AsSpan
因为存在从 T[]
.
我相信*最终写入的字节数将始终与 HashSize
的长度相同。 If/when 是的,我们只是简单地利用了原始数组。否则我们需要根据写入的哈希字节数将其切片到正确的长度。
如果缓冲区不够大,TryComputeHash
returns false
我们会抛出异常。我非常肯定这不会发生在我们身上,因为我们是根据 HashSize
显式计算大小,但无论如何我们都会将这种情况作为最佳实践来处理。
我已经通过了 CancellationToken.None
,但您可以提供自己的令牌。我还使用 Range
语法而不是显式调用 Slice
。如果这对您不可用或者您只是不喜欢它的外观,您可以明确说明:
/*1*/ memory.Memory.Span.Slice(0, bytesRead)
/*2*/ tempBuff.AsSpan(0, hashWritten).ToArray()
我们可以做出的一些可能假设:
- 假设
HashSize
总是8的倍数 - 假设
HashSize
始终等于写入的字节数并且不对最终数组进行切片 - 假设我们总是提供足够大的缓冲区(按照上面的说法,这就是所需的确切大小)并删除
if
和Exception
while ((bytesRead = await inputStream.ReadAsync(memory.Memory, CancellationToken.None)) > 0)
{
var tempBuff = new byte[this.Algorithm.HashSize/8];
_ = this.Algorithm.TryComputeHash(memory.Memory.Span[..bytesRead], tempBuff, out _);
chunkHashes[idx++] = tempBuff;
}
* 不幸的是,我不能 100% 肯定地说这些都是有效的假设。我看过的大多数实现的源代码都有一个 Debug.Assert
验证缓冲区大小和写入的字节数是相同的,所以我认为它们是 合理的 。也就是说,我想我个人会坚持使用更详细的选项。
您还会注意到我已经删除了您的 ComputeHash
功能。这并不是说您仍然不能使用它,但我将把它转换为这种基于 Try
的 Memory<>
模式作为对 reader.