C# 中的安全零拷贝

英文链接
Safe zero-copy operations in C#
C# 中的安全零拷贝

Sedat Kapanoglu 塞达特·卡帕诺卢 29 Sep 2025

My attempt at talking about one of the most underrated features of C#.
我试图谈论 C#中最被低估的功能之一。

 

image

 

C# is a versatile language. You can write mobile apps, desktop apps, games, websites, services and APIs with it. You can write it like Java with all the abstractions and AbstractionFactoryClassProviders. But differently from Java, you can write low-level and unsafe code too. When I say low-level, I mean without the GC, with raw pointers.
C#是一种通用语言。你可以用它编写移动应用程序、桌面应用程序、游戏、网站、服务和API。你可以像Java一样用所有抽象和AbstractionFactoryClassProvider编写它。但与Java不同,您也可以编写低级和不安全的代码。当我说低级时,我的意思是没有GC,有原始指针。

Low-level code is usually required for performance or interoperability with C libraries or the operating system. The reason low-level code helps with performance is that it can be used to eliminate runtime checks on memory accesses.
底层代码通常是为了性能或与 C 库或作系统的互作性所必需。低层代码有助于提升性能的原因在于,它可以用来消除内存访问的运行时检查。

Array element accesses are bounds-checked in C# for safety. But, that means that there's performance impact unless the compiler can eliminate a bounds-checking operation. The bounds-checking elimination logic needs to ensure that the array index was already bounds-checked before, or can be assured to be inside bounds during the compile-time. For example, take this simple function:
数组元素访问在 C#中会进行边界检查以确保安全 。但这意味着除非编译器能消除边界检查作,否则会有性能影响。边界检查消除逻辑需要确保数组索引在编译过程中已经被边界检查过,或者可以保证在编译过程中处于边界内。例如,考虑这个简单的函数:

int sum(int[] array)
{
int sum = 0;
for (int i = 0; i < array.Length; i++)
{
sum += array[i];
}
return sum;
}
That's an ideal situation for bounds-checking elimination because the index variable i is created with known boundaries, and it depends on the array's length. The index variable's lifetime is shorter than the array's lifetime and it's guaranteed to be contained valid values throughout the function. The native code produced for sum has no bounds-checking:
这对于边界检查消除来说是理想的情况,因为索引变量 i 是用已知边界创建的,且取决于数组的长度。索引变量的寿命比数组的寿命短,且保证其在整个函数中都包含有效值。为求和生成的本地代码没有边界检查:

L0000 xor eax, eax
L0002 xor edx, edx
L0004 mov r8d, [rcx+8] ; read length
L0008 test r8d, r8d ; is empty?
L000b jle short L001c ; skip the loop
L000d mov r10d, edx
L0010 add eax, [rcx+r10*4+0x10] ; sum += array[i];
L0015 inc edx ; i++
L0017 cmp r8d, edx ; compare length with i
L001a jg short L000d ; loop if still greater
L001c ret
But, what if the function signature was slightly different?
但是,如果函数特征稍有不同呢?

int sum(int[] array, int startIndex, int endIndex)
{
int sum = 0;
for (int i = startIndex; i <= endIndex; i++)
{
sum += array[i];
}
return sum;
}
Now, the C# compiler doesn't have a way to know if the passed startIndex and endIndex values are inside the boundaries of array because their lifetimes are distinct. So the native assembly produced becomes way more involved with bounds-checking operations:
C# 编译器无法判断传出的 startIndex 和 endIndex 值是否在数组边界内,因为它们的寿命不同。因此,生成的原生装配会更加复杂地涉及边界检查作:

L0000 sub rsp, 0x28
L0004 xor eax, eax ; sum = 0
L0006 cmp edx, r8d ; startIndex > endIndex?
L0009 jg short L0045 ; then skip the entire function
L000b test rcx, rcx ; array is null?
L000e je short L0031 ; then cause NullReferenceException
L0010 mov r10d, edx
L0013 or r10d, r8d
L0016 jl short L0031
L0018 cmp [rcx+8], r8d ; array.Length <= endIndex ?
L001c jle short L0031 ; then do bounds-checking
L001e xchg ax, ax ; alignment NOP
L0020 mov r10d, edx
L0023 add eax, [rcx+r10*4+0x10] ; sum += array[i]
L0028 inc edx ; consider i + 1
L002a cmp edx, r8d ; i > endIndex?
L002d jle short L0020 ; no, go on
L002f jmp short L0045 ; return
L0031 cmp edx, [rcx+8] ; i > array.Length?
L0034 jae short L004a ; bounds-checking failed. go to ----+
L0036 mov r10d, edx |
L0039 add eax, [rcx+r10*4+0x10] ; sum += array[i] |
L003e inc edx ; i++ |
L0040 cmp edx, r8d ; i <= endIndex ? |
L0043 jle short L0031 ; continue for loop |
L0045 add rsp, 0x28 |
L0049 ret ; return sum |
L004a call 0x00007ff857ec6200 ; throw IndexOutOfRangeException <--+
We could use low-level unsafe functions and pointers in C# (yes, C# supports raw pointers!) to avoid bounds-checking altogether, like this:
我们可以在C#中使用低级不安全函数和指针(是的,C#支持原始指针!)来完全避免边界检查,如下所示:

unsafe int sum(int* ptr, int length)
{
int* end = ptr + length;
int sum = 0;
for (; ptr < end; ptr++)
{
sum += *ptr;
}
return sum;
}
That also creates very optimized code that supports passing along a sub-portion of an array:
这也创建了非常优化的代码,支持传递数组的子部分:

L0000 movsxd rax, edx
L0003 lea rax, [rcx+rax*4] ; end = ptr + length
L0007 xor edx, edx ; sum = 0
L0009 cmp rcx, rax ; ptr >= end ?
L000c jae short L0019 ; then return
L000e add edx, [rcx] ; sum += *ptr
L0010 add rcx, 4 ; ptr += sizeof(int)
L0014 cmp rcx, rax ; ptr < end?
L0017 jb short L000e ; then keep looping
L0019 mov eax, edx
L001b ret ; return sum
Unsafe code and pointer-arithmetic can be very performant as you can see. The problem is that it's too dangerous. With incorrect values of length, you don't simply get an IndexOutOfRangeException, but instead, your app either crashes, or returns incorrect results. If your code happened to modify the memory region instead of just reading it, then you could have a nice entry point for a buffer overflow security vulnerability in your app too. Not to mention that all the callers of that function will have to have unsafe blocks too.
如您所见,不安全代码和指针算法的性能非常高。问题是它太危险了。如果长度值不正确,您不会简单地得到IndexOutOfRangeException,相反,您的应用程序要么崩溃,要么返回不正确的结果。如果你的代码碰巧修改了内存区域,而不仅仅是读取它,那么你的应用程序中也可能存在缓冲区溢出安全漏洞的一个很好的入口点。更不用说该函数的所有调用者也必须有不安全的块。

But it's possible to handle this safe and fast in C# without resorting to esoteric rituals like that. First, how do you solve this problem of indexes to describe a portion of an array and actual boundaries of the array being disconnected from each other? You create a new immutable type that holds these values together. And that type is called a span in C#. Other programming languages may call it a slice. Declaration of Span type resembles something like this. Well, it's not exactly this, but I want you to understand the concept first:
但是,在C#中可以安全快速地处理这个问题,而无需诉诸这样的深奥仪式。首先,如何解决索引描述数组的一部分和数组的实际边界彼此断开连接的问题?您创建了一个新的不可变类型,将这些值保存在一起。在C#中,这种类型被称为span。其他编程语言可能称之为切片。Span类型的声明类似于这样。好吧,不完全是这样,但我想让你先理解这个概念:

readonly struct Span<T>
{
readonly T* _ptr;
readonly int _len;
}
It's basically an immutable pointer with length. The great thing about a type like this is that the compiler can assure that once an immutable Span is initialized with correct bounds, it will always be safe to access without any bounds-checking. That means, you can pass around sub-views of arrays or even other spans safely and quickly without the performance overhead.
它基本上是一个长度不可变的指针。像这样的类型的好处是,编译器可以确保一旦用正确的边界初始化了不可变的Span,在不进行任何边界检查的情况下访问它总是安全的。这意味着,您可以安全快速地传递数组甚至其他跨度的子视图,而不会产生性能开销。

But, how can it be safe? What if the GC decides to throw away the structure that ptr points to? Well, that's where "ref types" come into play in C#.
但是,它怎么可能是安全的呢?如果GC决定丢弃ptr指向的结构怎么办?好吧,这就是“ref类型”在C#中发挥作用的地方。

A ref type is a type that can't leave the stack and escape to the heap, so it's always guaranteed that a T instance will outlive a Span<T> instance derived from it. That's why the actual Span<T> declaration looks like this:
ref类型是一种不能离开堆栈并逃逸到堆的类型,因此总是保证t实例的寿命会超过从其派生的Span<t>实例。这就是为什么实际的Span<t>声明看起来像这样:

readonly ref struct Span<T> // notice "ref"
{
readonly ref T _ptr; // notice "ref"
readonly int _len;
}
Since a ref type can only live in stack, it can't be a member of a class, nor can it be assigned to a non-ref variable, like, it can't be boxed either. A ref type can only be contained inside another ref type. It's ref types all the way down.
由于ref类型只能存在于堆栈中,因此它不能是类的成员,也不能被分配给非ref变量,例如,它也不能被装箱。引用类型只能包含在另一个引用类型中。一路往下都是ref类型。

Span-based version of our sum function can eliminate bounds-checking, and it can have other super powers too. The first one is that it can receive a sub-view of an array:
基于跨度的求和函数版本可以消除边界检查,它还可以具有其他超能力。第一个是它可以接收数组的子视图:

int sum(Span<int> span)
{
int sum = 0;
for (int i = 0; i < span.Length; i++)
{
sum += span[i];
}
return sum;
}
For instance, you can call this function with sum(array) or you can call it with a sub-view of an array like sum(array[startIndex..endIndex]). That wouldn't incur new bounds-checking operations other than when you're trying to slice the array using the range operator. See how the generated assembly for sum becomes optimized again:
例如,您可以使用sum(array)调用此函数,也可以使用类似sum(array[startIndex..endIndex])的数组的子视图调用它。这不会引发新的边界检查操作,除非您尝试使用范围运算符对数组进行切片。看看生成的sum程序集是如何再次优化的:

L0000 mov rax, [rcx]
L0003 mov ecx, [rcx+8]
L0006 xor edx, edx ; sum = 0
L0008 xor r8d, r8d ; i = 0
L000b test ecx, ecx ; span.Length == 0?
L000d jle short L001e
L000f mov r10d, r8d
L0012 add edx, [rax+r10*4] ; sum += span[i]
L0016 inc r8d ; i++
L0019 cmp r8d, ecx ; i < Length?
L001c jl short L000f ; then keep looping
L001e mov eax, edx
L0020 ret ; return sum
Another super power you get is the ability to declare the data structure that you receive as "immutable" in your function signature, so that function is forbidden from changing it, and you can find relevant bugs instantly. All you need to do is to replace Span<T> with ReadOnlySpan<T>. Then your attempts to modify the span contents will immediately cause a compiler error. Something impossible with regular arrays, even if you declare them readonly. The readonly directive only protects the reference from modification not the contents of the data structure.
你得到的另一个超能力是在函数签名中将你收到的数据结构声明为“不可变”的能力,这样函数就被禁止更改它,你可以立即发现相关的错误。您需要做的就是将Span<T>替换为ReadOnlySpan<T>。然后,您尝试修改span内容将立即导致编译器错误。常规数组是不可能的,即使你声明它们是只读的。只读指令仅保护引用不被修改,而不保护数据结构的内容。

How is it related to zero-copy?
它和零拷贝有什么关系?
Passing along a smaller portion of a larger data structure to relevant APIs used to involve either copying or passing the relevant part's offset and length values along with the data structure. It required the API to have overloads with ranges. It was impossible to guarantee the safety of such APIs as the relationship between parameters couldn't be established by the compiler or the runtime.
过去,将较大数据结构的较小部分传递给相关API,这些API用于复制或传递相关部分的偏移量和长度值以及数据结构。它要求API具有带范围的重载。由于编译器或运行时无法建立参数之间的关系,因此无法保证此类API的安全性。

It's now both easy and expressive to implement zero-copy operations safely using spans. Consider a Quicksort implementation for instance; it usually has a function like this that works with portions of a given array:
现在,使用span安全地实现零拷贝操作既简单又富有表现力。例如,考虑一个Quicksort实现;它通常有一个这样的函数,可以处理给定数组的部分:

int partition(int[] array, int low, int high)
{
int midpoint = (high + low) / 2; // I know, we'll get there!
int mid = array[midpoint];

// tuple swaps in C#! ("..^1" means "Length - 1")
(array[midpoint], array[^1]) = (array[^1], array[midpoint]);
int pivotIndex = 0;
for (int i = low; i < high - 1; i++)
{
if (array[i] < mid)
{
(array[i], array[pivotIndex]) = (array[pivotIndex], array[i]);
pivotIndex += 1;
}
}
(array[midpoint], array[^1]) = (array[^1], array[midpoint]);
return pivotIndex;
}
This function receives an array, start and end offsets into the array, and rearranges the items based on a picked value in it. Values smaller than the picked value move to the left, larger values move to the right.
此函数接收一个数组,将开始和结束偏移量放入数组中,并根据其中拾取的值重新排列项目。小于拾取值的值向左移动,较大的值向右移动。

The mid = array[midpoint] has to be bounds-checked because the compiler can't know if the index is inside the bounds of this array. The for loop also performs bounds-checking for array accesses in the loop, which some of them can be eliminated, but not fully guaranteed.
mid = array[midpoint] 必须检查边界,因为编译器无法知道索引是否在该数组的边界内。for循环还对循环中的数组访问执行边界检查,其中一些可以消除,但不能完全保证。

There is also an overflow error because we pass array ranges using high and low values: (high+low) can overflow for very large arrays, and the results would be catastrophic, can even cause buffer overflow exceptions.
还有一个溢出错误,因为我们使用高值和低值传递数组范围:(高+低)对于非常大的数组可能会溢出,结果将是灾难性的,甚至可能导致缓冲区溢出异常。

The partition function gets recursively called many times by Quicksort function below. That means bounds-checking can be a performance issue.
下面的Quicksort函数会多次递归调用分区函数。这意味着边界检查可能是一个性能问题。

void Quicksort(int[] array, int low, int high)
{
if (array.Length <= 1)
{
return;
}

int pivot = partition(array, low, high);
Quicksort(span, low, pivot - 1);
Quicksort(span, pivot + 1, high);
}
With spans, the same Quicksort function looks like this:
使用span,相同的快速排序函数看起来像这样:

void Quicksort(Span<int> span)
{
if (span.Length <= 1)
{
return;
}

int pivot = partition(span);
Quicksort(span[..pivot]);
Quicksort(span[(pivot + 1)..]);
}
See how expressive using spans are especially with the range syntax? It lets you get a new span out of an existing span or an array using double dots (..). Even the partition function looks much better:
看看使用span的表现力有多强,尤其是在range语法中?它允许您使用双点(..)从现有跨度或数组中获取新跨度。即使是分区函数看起来也要好得多:

int partition(Span<int> span)
{
int midpoint = span.Length / 2; // look ma, no buffer overflows!
int mid = span[midpoint];
(span[midpoint], span[^1]) = (span[^1], span[midpoint]);
int pivotIndex = 0;
for (int i = 0; i < span.Length - 1; i++)
{
if (span[i] < mid)
{
(span[i], span[pivotIndex]) = (span[pivotIndex], span[i]);
pivotIndex += 1;
}
}
(span[midpoint], span[^1]) = (span[^1], span[midpoint]);
return pivotIndex;
}
Because C# spans are also zero-based, it's harder to have buffer overflow problems caused by formulae like (low + high) / 2. Now, the implementation is as fast as an unsafe implementation with raw pointers, but still extremely safe.
因为C#跨度也是从零开始的,所以很难出现由(低+高)/2等公式引起的缓冲区溢出问题。现在,该实现与使用原始指针的不安全实现一样快,但仍然非常安全。

New zero-copy operations in .NET runtime
.NET 运行时中新增了零拷贝操作。
I used examples that use recursive calls to show how sub-portions of a larger data structure can be passed to another function without copying, but spans can be used almost everywhere, and now .NET runtime supports zero-copy alternatives of popular functions too.
我使用了使用递归调用的示例来展示如何在不复制的情况下将较大数据结构的子部分传递给另一个函数,但span几乎可以在任何地方使用,现在也是如此。NET运行时也支持流行函数的零拷贝替代方案。

Take String.Split for example. You can now split a string without creating new copies of every split portion of the string. You can split a CSV line into its parts like this:
以String.Split为例。现在,您可以拆分字符串,而无需为字符串的每个拆分部分创建新副本。您可以将CSV行拆分为以下部分:

string csvLine = // .. read CSV line
string[] parts = csvLine.Split(',');

foreach (string part in parts)
{
Console.WriteLine(part);
}
The problem with that is, now you're dealing with five new memory blocks with varying lengths. .NET allocates memory for them, GC keeps track of them. It's slow, it hogs memory. It's problematic especially in loops, and can create GC pressure, slowing your app even more.
问题是,现在你正在处理五个不同长度的新内存块。 .NET为它们分配内存,GC跟踪它们。它很慢,占用了记忆。这是有问题的,尤其是在循环中,可能会产生GC压力,进一步减缓你的应用程序。

You can instead cast your CSV line into a ReadOnlySpan<char> and iterate over its components to write it to the output:
您可以将CSV行转换为ReadOnlySpan<char>,并遍历其组成部分,写入输出:

string csvLine = // .. read CSV line

var span = csvLine.AsSpan();
var parts = span.Split(',');
foreach (var range in parts)
{
Console.Out.WriteLine(span[range]);
}
Note that we use a small detour to use Console.Out.WriteLine instead of Console.WriteLine because Console class lacks an overload to output a ReadOnlySpan<char> like a string.
注意我们会用 Console.Out.WriteLine 代替 Console.WriteLine, 因为Console类缺少重载来像字符串一样输出ReadOnlySpan<char>。

The great ROI of that experiment is making zero memory allocations after reading CSV line into memory. A similar logic that improves performance and memory efficiency can be applied everywhere that can receive a Span<T>/ReadOnlySpan<T> instead of an array.
该实验的最大投资回报率是在将CSV行读入内存后实现零内存分配。提高性能和内存效率的类似逻辑可以应用于任何可以接收Span<T>/ReadOnly Span<T>而非数组的地方。

Embrace the future 拥抱未来
Spans and slice-like structures in are the future of safe memory operations in modern programming languages. Embrace them. The quick takeaways are:
Spans and slice-like 结构是现代编程语言中安全内存操作的未来。拥抱他们。简要总结如下:

Use spans over arrays in your function declarations unless you explicitly require a standalone array in your function for some reason. Such a change opens your API into zero-copy optimization scenarios, and calling code will be more expressive.
除非你明确要求函数中存在独立数组,否则在函数声明中使用spans代替数组。这样的改变会让你的 API 进入零拷贝优化场景,调用代码时也会更有表现力。
Don't bother with unsafe/pointers if you can code the same logic with spans. You can still perform low-level memory operations without wandering into the dangerous parts of the forest. Your code will still be fast, and yet safer.
如果你能用 span 编写同样的逻辑,就不用操心 unsafe 或指针。你仍然可以执行低级内存操作,而不必误入森林的危险区域。你的代码依然快速,但更安全。
Use spans, wherever possible, mostly readonly.
尽可能使用spans,主要是readonlyspan。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/978888.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

适合应届生:零经验专业简历模板TOP4

对于即将踏入职场的应届毕业生而言,最头疼的莫过于“零经验”这一挑战。如何在简历中巧妙展现自身潜力,赢得HR的青睐?选择一款合适的简历制作工具显得尤为重要。 本文将为您盘点值得应届生信赖的四大简历模板工具,…

Proofpoint Satori威胁情报代理正式登陆Microsoft Security Copilot平台

Proofpoint宣布其Satori新兴威胁情报代理正式在Microsoft Security Copilot平台上线。该代理整合了全球分布式传感器网络和第三方威胁数据源,帮助安全团队快速识别被主动利用的漏洞并优先修复,提升基于风险的漏洞管理…

经济学数据如何优化员工体验的技术实践

文章探讨了如何运用经济学数据和科学模型分析员工行为模式,通过数据驱动方法优化人力资源政策,包括保险参与率分析和空间政策影响评估等技术手段。经济学数据如何为更公平的员工体验提供支持 作为全球规模最大、最多…

2025年简约智能家居照明灯品牌推荐,让生活更智能

在现代家居生活中,简约智能家居照明灯已成为提升家居氛围的重要元素。选择可靠的照明灯工厂和优质的供应商,能帮助消费者获得更高性价比的产品。本篇文章将推荐2025年最佳的智能家居照明灯品牌。通过深入分析各大厂家…

AT_fps_24_a お菓子

显然设生成函数 \(F(x)=x+x^3+x^4+x^6\),然后答案就是 \([x^n]F(x)^D\)。 \((x+x^3+x^4+x^6)^D=x^D(1+x^2+x^3+x^5)^D=x^D(1+x^2)^D(1+x^3)^D\)

NOIP 2025 游记(?

NOIP 2025 游记(?以下是博客签名,正文无关 本文来自博客园,作者:Wy_x,转载请在文首注明原文链接:https://www.cnblogs.com/Wy-x/p/19279179 版权声明:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许…

[论文阅读] AI | 大语言模型服务框架服务级目标和系统级指标优化研究

[论文阅读] AI | 大语言模型服务框架服务级目标和系统级指标优化研究pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: &qu…

2025年11月治鼻炎产品推荐:高性价比解决方案与市场热门排行榜

随着环境污染和气候变化加剧,鼻炎已成为影响大众生活质量的常见问题。许多用户在选择治鼻炎产品时面临诸多困惑,包括产品安全性、适用人群、疗效持久性等关键因素。根据行业数据分析,当前鼻炎治疗产品市场呈现多元化…

2025年11月地膜、农膜、塑料薄膜源头厂商最新推荐榜单:三光膜、大棚膜、水池布优质供应商选择指南

随着设施农业的快速发展,农膜、水池布等高分子覆盖材料成为提升作物品质与农业效益的关键。为破解地膜、农用塑料薄膜市场 “品质参差、选品困难” 痛点,本次榜单基于技术创新力、产品效能、地域适配性三大维度,结合…

蓝牙音频协议——安卓开发

协议 AVRCP(Audio Vidoe Remote Control Protocol,音频视频远程控制协议),区分为CT(Control)和TG(Target)两端,TG就是受控端。A2DP(Advenced Audio Distribution Profile,即蓝牙音频传输模型协定),和音频模型一样…

2025年Q4痔疮膏品牌哪家好?TOP10测评榜单,内痔便血/外痔肉球/术后修护全适配推荐

一、行业背景与测评体系核心说明 (一)行业背景:高患病率下的用药困局 据《2025中国肛肠健康管理白皮书》披露,我国痔疮患者已达6.7亿,25-50岁职场人群患病率突破85%,学生党患病率较5年前上升23%,呈现“高基数、…

2025年11月治鼻炎产品推荐:一份详尽的清单与选择指南

随着气候变化和环境污染加剧,鼻炎已成为困扰各年龄段人群的常见健康问题。根据国家卫生健康委员会2024年发布的慢性呼吸系统疾病调研数据,我国过敏性鼻炎患病率呈逐年上升趋势,尤其在春秋季节高发。许多鼻炎患者面临…

GitHub 热榜项目 - 日榜(2025-11-01) - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

成为中国中小制造业企业数字营销领域的引领者 ——纪实西安动力无限的信息化赋能之路

在数字经济浪潮奔涌向前的今天,如何让扎根于实体、承载着就业与创新的中小制造业企业,搭上互联网快车,实现品牌突围与市场拓展,是一个关乎区域经济乃至国家产业发展的重要课题。在西北重镇西安,一家名为“动力无限…

2025年Q4国内AI搜索优化公司排行榜,最新口碑认证+AI平台适配测评推荐

一、行业背景:生成式AI搜索进入深水区,“语义力+转化力”成竞争核心2025年,AI搜索已从辅助工具升级为用户获取信息的核心入口,用户路径从“Search→ShortVideo→AskAI”加速迁移,品牌在DeepSeek、ChatGPT等平台的…

2025年11月治鼻炎产品推荐:高性价比产品排行榜与使用评价

随着秋冬季节交替,气温波动加剧,鼻炎患者往往面临症状反复发作的困扰。选择一款安全有效的鼻炎产品成为许多用户的迫切需求。根据国家卫生健康委员会发布的呼吸健康数据,我国鼻炎患者基数较大,且呈现年轻化趋势。用…