使用 C# Span<T> 实现高性能应用
使用 C# Span<T> 实现高性能应用
简介
Span 是一个结构类型(struct
),在 C# 7.2 中作为 System
命名空间下的 Span<T>
结构引入。它的设计目标是表示一块连续的任意内存区域。与数组或集合不同,Span
并不拥有它所指向的内存区域,而是为现有的内存块提供了一个轻量级的视图。这种特性使 Span
在需要高效处理内存缓冲区的场景中尤其强大,同时避免了额外的开销和不安全代码的使用。
- 非拥有性质
Span
是一种非拥有类型,这意味着它不会分配或释放托管内存或非托管内存。它操作的是现有的内存块,因此在内存所有权由其他地方管理或在多个组件之间共享的场景下,Span
是一个理想的选择。 - 连续内存
Span
表示一段连续的内存区域。由于这种连续性,Span
可以与其他基于内存的结构(如数组、指针和本机互操作场景)无缝交互。 - 性能优势
Span
的非拥有和连续性特点使其具有显著的性能优势。由于它不涉及内存分配或复制,使用Span
可以让代码执行更高效、更快速。 - 零成本抽象
Span
的设计原则之一是提供零成本抽象。这意味着在代码中使用Span
不会引入任何运行时开销,因此适用于对性能要求极高的场景。
在需要避免不必要的字符串分配并提升性能的场景中,ReadOnlySpan
是一个更好的选择,尤其是在处理大型字符串或执行子字符串操作时。ReadOnlySpan<char>
在需要只读访问字符串的某一部分且无需创建新的字符串对象时非常有用。以下是一些常见用法的介绍:
1. 从字符串创建 ReadOnlySpan
通过 AsSpan
方法可以轻松从字符串创建一个 ReadOnlySpan<char>
。
string originalString = "Hello, World!";
ReadOnlySpan<char> spanFromString = originalString.AsSpan();
2. 使用子字符串
与 Substring
不同,可以使用 Slice
方法操作 ReadOnlySpan<char>
。
ReadOnlySpan<char> substringSpan = spanFromString.Slice(startIndex, length);
. 将子字符串传递给方法
在将子字符串传递给方法时,可以使用 ReadOnlySpan<char>
代替普通的字符串。
void ProcessSubstring(ReadOnlySpan<char> substring)
{
// 对子字符串进行操作
}
// 调用
ProcessSubstring(spanFromString.Slice(startIndex, length));
4. 在字符串中搜索
可以在 ReadOnlySpan<char>
上使用 IndexOf
方法进行搜索。
int index = spanFromString.IndexOf('W');
5. 使用内存映射文件
在处理大文件时(例如内存映射文件),使用 ReadOnlySpan<char>
更加高效。
using (MemoryMappedFile mmf = MemoryMappedFile.CreateFromFile(""))
{
using (MemoryMappedViewAccessor accessor = mmf.CreateViewAccessor())
{
long fileSize = new FileInfo("").Length;
ReadOnlySpan<byte> fileData = accessor.ReadArray(0, (int)fileSize).Span;
// 以 ReadOnlySpan<byte> 处理 fileData
}
}
在某些场景中,ReadOnlySpan<char>
可以用于高效地处理字符串操作。
// 在子字符串中替换字符而不创建新字符串
spanFromString.Slice(startIndex, length).CopyTo(newSpan);
7. 将子字符串传递给 API
某些 API 出于性能考虑可能会接受 ReadOnlySpan<char>
。例如,在使用操作字符范围的外部库或 API 时。
void ExternalApiMethod(ReadOnlySpan<char> data)
{
// 使用字符范围调用外部 API
}
// 使用示例
ExternalApiMethod(spanFromString.Slice(startIndex, length));
ReadOnlySpan<char>
提供了一种更高效处理字符串的方式,尤其是在需要尽量减少内存分配和复制的场景下。它是优化性能关键代码的强大工具,在处理大量字符串数据时尤为有用。
虽然 C# 的 Span
功能强大且优势明显,但它在处理连续和非连续内存缓冲区时也存在一些局限性和需要注意的事项。以下是这些局限性详解。
连续内存缓冲区
- 内存所有权
Span
是一种非拥有类型,它不拥有它所指向的内存。这意味着你需要确保在Span
的生命周期内,底层内存或非托管内存保持有效。如果内存实例被释放或变得无效,继续使用Span
会导致未定义行为。 - 不可变字符串
虽然
Span
被设计用于高效操作可变内存,但 C# 的字符串是不可变的。将字符串转换为Span<char>
时,可能会引发意外问题,尤其是在尝试修改字符串内容时。 - 数组边界检查
虽然
Span
本身提供了零成本抽象,但对Span
的操作并未消除数组边界检查。这意味着在通过Span
访问元素时,运行时仍会进行数组边界检查,与使用不安全指针相比可能带来轻微的性能开销。 - 垃圾回收的影响
如果你在数组上创建了一个
Span
,而该数组被垃圾回收器回收,那么随后使用Span
会导致未定义行为。这是因为底层内存可能已被回收,而通过Span
访问它可能会导致访问无效内存。 - 某些 API 的兼容性
一些 API 或库可能不直接支持
Span
,尤其是较旧的或未设计为支持Span
的第三方库。在这些情况下,你可能需要在Span
和其他类型(如数组或指针)之间进行转换。
非连续内存缓冲区
- 对非连续内存的有限支持
Span
主要设计用于处理连续内存缓冲区或块。在需要处理非连续内存缓冲区或具有内存间隙的结构时,Span
可能不是最合适的选择。 - 结构化局限性
某些数据结构或涉及非连续内存的场景可能不适合
Span
。例如,链表或图结构可能无法满足Span
对连续内存的要求。 - 复杂指针操作
在处理非连续内存时,尤其是需要复杂指针运算的场景,
Span
可能无法提供与 C++ 中原生指针相同的底层控制和灵活性。在这些情况下,使用不安全代码和指针可能会更适合。 - 某些 API 的直接支持不足
与连续内存类似,某些 API 或库可能不直接支持通过
Span
表示的非连续内存。这种情况下可能需要额外的中间步骤或转换。
在 C# 中,Span
可以高效地与非托管内存结合使用,以一种受控且高效的方式执行内存相关操作。非托管内存是指不受 .ET 运行时垃圾回收器管理的内存,通常涉及原生内存的分配和释放。以下是如何在 C# 中使用 Span
操作非托管内存的示例:
分配非托管内存
可以使用 System.Runtime.InteropServices
命名空间下的 Marshal
类来分配非托管内存。Marshal.AllocHGlobal
方法用于分配非托管内存并返回分配块的指针。分配的内存区域具有读写权限,并且可以通过 Span
轻松访问。
using System;
using System.Runtime.InteropServices;
classProgram
{
static void Main()
{
ctint bufferSize = 100;
IntPtr unmanagedMemory = Marshal.AllocHGlobal(bufferSize);
// 从非托管内存创建 Span
Span<byte> span = new Span<byte>(unmanagedMemory.ToPointer(), bufferSize);
// 根据需要使用 Span...
// 完成后不要忘记释放非托管内存
Marshal.FreeHGlobal(unmanagedMemory);
}
}
在这个例子中,我们使用 Marshal.AllocHGlobal
分配了一块非托管内存,并通过获取的指针创建了一个 Span<byte>
。这样可以利用 Span
的 API 操作非托管内存。
复制数据到非托管内存或从非托管内存中复制数据
Span
提供了 Slice
、CopyTo
和 ToArray
等方法,用于在托管和非托管内存之间高效地复制数据。
using System;
using System.Runtime.InteropServices;
classProgram
{
static void Main()
{
ctint bufferSize = 100;
IntPtr unmanagedMemory = Marshal.AllocHGlobal(bufferSize);
// 从非托管内存创建 Span
Span<byte> span = new Span<byte>(unmanagedMemory.ToPointer(), bufferSize);
// 将数据复制到非托管内存
byte[] dataToCopy = { 1, 2, , 4, 5 };
dataToCopy.AsSpan().CopyTo(span);
// 从非托管内存复制数据
byte[] copiedData = span.ToArray();
// 完成后不要忘记释放非托管内存
Marshal.FreeHGlobal(unmanagedMemory);
}
}
在这个示例中,我们通过 CopyTo
将托管数组的数据复制到非托管内存,随后通过 ToArray
将数据从非托管内存复制回托管数组。这种方法在需要在托管和非托管内存之间高效传输数据时非常有用。
使用不安全代码
在处理非托管内存时,也可以结合不安全代码使用指针。在这种情况下,可以通过 GetPinnableReference
方法从 Span
中获取指针。
using System;
using System.Runtime.InteropServices;
classProgram
{
static void Main()
{
ctint bufferSize = 100;
IntPtr unmanagedMemory = Marshal.AllocHGlobal(bufferSize);
// 从非托管内存创建 Span
Span<byte> span = new Span<byte>(unmanagedMemory.ToPointer(), bufferSize);
// 使用不安全代码处理指针
unsafe
{
byte* pointer = (byte*)Unsafe.AsPointer(ref MemoryMarshal.GetReference(span));
// 根据需要使用指针...
}
// 别忘了释放非托管内存
Marshal.FreeHGlobal(unmanagedMemory);
}
}
在上述示例中,我们通过 Unsafe.AsPointer
方法从 Span
中获取一个指针。这使得我们可以直接使用不安全代码对内存进行操作。
注意:在处理非托管内存时,务必正确管理内存的分配与释放,以避免内存泄漏。同时,使用不安全代码时需要格外小心,因为一旦操作不当,可能会引发安全风险。
Span 与异步方法调用
将 Span
与 C# 中的异步方法结合使用是一个强大的组合,特别是在处理大量数据或 I/O 操作时,可以有效避免数据的额外拷贝。以下是一些常见场景:
1. 异步 I/O 操作
在异步读取或写入流数据时,可以使用 Memory<T>
或 Span<T>
高效地操作数据,避免创建额外的缓冲区。
async Task ProcessDataAsync(Stream stream)
{
ctint bufferSize = 4096;
byte[] buffer = newbyte[bufferSize];
while (true)
{
int bytesRead = await stream.ReadAsync(buffer.AsMemory());
if (bytesRead == 0)
break;
// 使用 Span 直接处理数据,避免不必要的拷贝
ProcessData(buffer.AsSpan(0, bytesRead));
}
}
void ProcessData(Span<byte> data)
{
// 对数据执行操作
}
在这个例子中,ReadAsync
方法异步读取流中的数据到缓冲区中,ProcessData
方法直接从 Span<byte>
中处理数据,无需额外拷贝。
2. 异步文件操作
类似于 I/O 操作,在处理文件的异步操作时,也可以使用 Span
高效地处理数据。
async Task ProcessFileAsync(string filePath)
{
ctint bufferSize = 4096;
using (FileStream fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read))
{
byte[] buffer = newbyte[bufferSize];
while (true)
{
int bytesRead = await fileStream.ReadAsync(buffer.AsMemory());
if (bytesRead == 0)
break;
// 使用 Span 直接处理数据,避免不必要的拷贝
ProcessData(buffer.AsSpan(0, bytesRead));
}
}
}
void ProcessData(Span<byte> data)
{
// 对数据执行操作
}
在这个示例中,ReadAsync
从文件流中读取数据到缓冲区中,然后 ProcessData
方法直接从 Span<byte>
中处理数据,避免了数据拷贝。
. 异步任务处理
当处理产生或消费数据的异步任务时,可以使用 Memory<T>
或 Span<T>
来避免不必要的拷贝。
async Task<int> ProcessDataAsync(int[] data)
{
// 异步处理数据
await Task.Delay(1000);
// 返回已处理数据的长度
return data.Length;
}
async Task Main()
{
int[] inputData = Enumerable.Range(1, 1000).ToArray();
// 异步处理数据,无需拷贝
int processedLength = await ProcessDataAsync(inputData.AsMemory());
Cole.WriteLine($"Processed data length: {processedLength}");
}
在此示例中,ProcessDataAsync
异步处理数据并返回数据的长度,而无需创建额外的数据副本。
总结
Span
是 C# 中一个强大的工具,它提供了一种高效的内存操作方式,特别适合在需要最小化内存分配和拷贝的场景中使用。由于其非拥有型和连续内存的特点,Span
在从字符串操作到高性能数值处理等多种应用中表现尤为出。通过正确使用 Span
,开发者可以显著优化代码性能,为构建高效、健壮的应用奠定基础。随着 C# 的不断演进,Span
无疑是优化代码的重要工具。
译文地址:c-sharpcorner/article/c-sharp-writing-high-performance-apps-using-spant/
本文参与 腾讯云自媒体同步曝光计划,分享自。原始发表:2025-01-08,如有侵权请联系 cloudcommunity@tencent 删除高性能内存数据字符串c##感谢您对电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格的认可,转载请说明来源于"电脑配置推荐网 - 最新i3 i5 i7组装电脑配置单推荐报价格
留言与评论(共有 15 条评论) |
本站网友 金鸡百花影城 | 27分钟前 发表 |
这是因为底层内存可能已被回收 | |
本站网友 北京新房房价 | 24分钟前 发表 |
例如 | |
本站网友 房屋产权证办理流程 | 21分钟前 发表 |
Span 的关键特性非拥有性质 Span 是一种非拥有类型 | |
本站网友 win7摄像头驱动下载 | 20分钟前 发表 |
它提供了一种高效的内存操作方式 | |
本站网友 阻生牙拔除术 | 9分钟前 发表 |
同时 | |
本站网友 血糖高吃什么好 | 16分钟前 发表 |
由于这种连续性 | |
本站网友 彩票预测软件 | 9分钟前 发表 |
ReadOnlySpan<char> 可以用于高效地处理字符串操作 | |
本站网友 重庆羽毛球 | 13分钟前 发表 |
ReadOnlySpan 的使用在需要避免不必要的字符串分配并提升性能的场景中 | |
本站网友 调理脾胃 | 10分钟前 发表 |
例如 | |
本站网友 抄板公司 | 13分钟前 发表 |
bytesRead)); } } } void ProcessData(Span<byte> data) { // 对数据执行操作 } 在这个示例中 | |
本站网友 长寿二手房网 | 5分钟前 发表 |
Span 与异步方法调用将 Span 与 C# 中的异步方法结合使用是一个强大的组合 | |
本站网友 肥胖标准 | 9分钟前 发表 |
数组边界检查 虽然 Span 本身提供了零成本抽象 | |
本站网友 上海合租 | 26分钟前 发表 |
那么随后使用 Span 会导致未定义行为 | |
本站网友 中国男子冰壶队 | 5分钟前 发表 |
非连续内存缓冲区对非连续内存的有限支持 Span 主要设计用于处理连续内存缓冲区或块 |