原文 | Stephen Toub

翻译 | 郑子铭

PGO

我在我的 .NET 6 性能改进一文中写了关于配置文件引导优化 (profile-guided optimization) (PGO) 的文章,但我将在此处再次介绍它,因为它已经看到了 .NET 7 的大量改进。

PGO 已经存在了很长时间,有多种语言和编译器。基本思想是你编译你的应用程序,要求编译器将检测注入应用程序以跟踪各种有趣的信息。然后你让你的应用程序通过它的步伐,运行各种常见的场景,使该仪器“描述”应用程序执行时发生的事情,然后保存结果。然后重新编译应用程序,将这些检测结果反馈给编译器,并允许它根据预期的使用方式优化应用程序。这种 PGO 方法被称为“静态 PGO”,因为所有信息都是在实际部署之前收集的,这是 .NET 多年来一直以各种形式进行的事情。不过,从我的角度来看,.NET 中真正有趣的开发是“动态 PGO”,它是在 .NET 6 中引入的,但默认情况下是关闭的。

动态 PGO 利用分层编译。我注意到 JIT 检测第 0 层代码以跟踪方法被调用的次数,或者在循环的情况下,循环执行了多少次。它也可以将它用于其他事情。例如,它可以准确跟踪哪些具体类型被用作接口分派的目标,然后在第 1 层专门化代码以期望最常见的类型(这称为“保护去虚拟化 (guarded devirtualization)”或 GDV)。你可以在这个小例子中看到这一点。将 DOTNET_TieredPGO 环境变量设置为 1,然后在 .NET 7 上运行:

class Program
{
static void Main()
{
IPrinter printer = new Printer();
for (int i = 0; ; i++)
{
DoWork(printer, i);
}
} static void DoWork(IPrinter printer, int i)
{
printer.PrintIfTrue(i == int.MaxValue);
} interface IPrinter
{
void PrintIfTrue(bool condition);
} class Printer : IPrinter
{
public void PrintIfTrue(bool condition)
{
if (condition) Console.WriteLine("Print!");
}
}
}

DoWork 的第 0 层代码最终看起来像这样:

G_M000_IG01:                ;; offset=0000H
55 push rbp
4883EC30 sub rsp, 48
488D6C2430 lea rbp, [rsp+30H]
33C0 xor eax, eax
488945F8 mov qword ptr [rbp-08H], rax
488945F0 mov qword ptr [rbp-10H], rax
48894D10 mov gword ptr [rbp+10H], rcx
895518 mov dword ptr [rbp+18H], edx G_M000_IG02: ;; offset=001BH
FF059F220F00 inc dword ptr [(reloc 0x7ffc3f1b2ea0)]
488B4D10 mov rcx, gword ptr [rbp+10H]
48894DF8 mov gword ptr [rbp-08H], rcx
488B4DF8 mov rcx, gword ptr [rbp-08H]
48BAA82E1B3FFC7F0000 mov rdx, 0x7FFC3F1B2EA8
E8B47EC55F call CORINFO_HELP_CLASSPROFILE32
488B4DF8 mov rcx, gword ptr [rbp-08H]
48894DF0 mov gword ptr [rbp-10H], rcx
488B4DF0 mov rcx, gword ptr [rbp-10H]
33D2 xor edx, edx
817D18FFFFFF7F cmp dword ptr [rbp+18H], 0x7FFFFFFF
0F94C2 sete dl
49BB0800F13EFC7F0000 mov r11, 0x7FFC3EF10008
41FF13 call [r11]IPrinter:PrintIfTrue(bool):this
90 nop G_M000_IG03: ;; offset=0062H
4883C430 add rsp, 48
5D pop rbp
C3 ret

而最值得注意的是,你可以看到调用[r11]IPrinter:PrintIfTrue(bool):这个做接口调度。但是,再看一下为第一层生成的代码。我们仍然看到调用[r11]IPrinter:PrintIfTrue(bool):this,但我们也看到了这个。

G_M000_IG02:                ;; offset=0020H
48B9982D1B3FFC7F0000 mov rcx, 0x7FFC3F1B2D98
48390F cmp qword ptr [rdi], rcx
7521 jne SHORT G_M000_IG05
81FEFFFFFF7F cmp esi, 0x7FFFFFFF
7404 je SHORT G_M000_IG04 G_M000_IG03: ;; offset=0037H
FFC6 inc esi
EBE5 jmp SHORT G_M000_IG02 G_M000_IG04: ;; offset=003BH
48B9D820801A24020000 mov rcx, 0x2241A8020D8
488B09 mov rcx, gword ptr [rcx]
FF1572CD0D00 call [Console:WriteLine(String)]
EBE7 jmp SHORT G_M000_IG03

第一块是检查IPrinter的具体类型(存储在rdi中)并与Printer的已知类型(0x7FFC3F1B2D98)进行比较。如果它们不一样,它就跳到它在未优化版本中做的同样的接口调度。但如果它们相同,它就会直接跳到Printer.PrintIfTrue的内联版本(你可以看到这个方法中对Console:WriteLine的调用)。因此,普通情况(本例中唯一的情况)是超级有效的,代价是一个单一的比较和分支。

这一切都存在于.NET 6中,那么为什么我们现在要谈论它?有几件事得到了改善。首先,由于dotnet/runtime#61453这样的改进,PGO现在可以与OSR一起工作。这是一个大问题,因为这意味着做这种接口调度的热的长期运行的方法(这相当普遍)可以得到这些类型的去虚拟化/精简优化。第二,虽然PGO目前不是默认启用的,但我们已经让它更容易打开了。在dotnet/runtime#71438dotnet/sdk#26350之间,现在可以简单地将true放入你的.csproj中。 csproj,它的效果和你在每次调用应用程序之前设置DOTNET_TieredPGO=1一样,启用动态PGO(注意,它不会禁止使用R2R图像,所以如果你希望整个核心库也采用动态PGO,你还需要设置DOTNET_ReadyToRun=0)。然而,第三,是动态PGO已经学会了如何检测和优化额外的东西。

PGO已经知道如何对虚拟调度进行检测。现在在.NET 7中,在很大程度上要感谢dotnet/runtime#68703,它也可以为委托做这件事(至少是对实例方法的委托)。考虑一下这个简单的控制台应用程序。

using System.Runtime.CompilerServices;

class Program
{
static int[] s_values = Enumerable.Range(0, 1_000).ToArray(); static void Main()
{
for (int i = 0; i < 1_000_000; i++)
Sum(s_values, i => i * 42);
} [MethodImpl(MethodImplOptions.NoInlining)]
static int Sum(int[] values, Func<int, int> func)
{
int sum = 0;
foreach (int value in values)
sum += func(value);
return sum;
}
}

在没有启用PGO的情况下,我得到的优化汇编是这样的。

; Assembly listing for method Program:Sum(ref,Func`2):int
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; Tier-1 compilation
; optimized code
; rsp based frame
; partially interruptible
; No PGO data G_M000_IG01: ;; offset=0000H
4156 push r14
57 push rdi
56 push rsi
55 push rbp
53 push rbx
4883EC20 sub rsp, 32
488BF2 mov rsi, rdx G_M000_IG02: ;; offset=000DH
33FF xor edi, edi
488BD9 mov rbx, rcx
33ED xor ebp, ebp
448B7308 mov r14d, dword ptr [rbx+08H]
4585F6 test r14d, r14d
7E16 jle SHORT G_M000_IG04 G_M000_IG03: ;; offset=001DH
8BD5 mov edx, ebp
8B549310 mov edx, dword ptr [rbx+4*rdx+10H]
488B4E08 mov rcx, gword ptr [rsi+08H]
FF5618 call [rsi+18H]Func`2:Invoke(int):int:this
03F8 add edi, eax
FFC5 inc ebp
443BF5 cmp r14d, ebp
7FEA jg SHORT G_M000_IG03 G_M000_IG04: ;; offset=0033H
8BC7 mov eax, edi G_M000_IG05: ;; offset=0035H
4883C420 add rsp, 32
5B pop rbx
5D pop rbp
5E pop rsi
5F pop rdi
415E pop r14
C3 ret ; Total bytes of code 64

注意其中调用[rsi+18H]Func`2:Invoke(int):int:this来调用委托。现在启用了PGO。

; Assembly listing for method Program:Sum(ref,Func`2):int
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; Tier-1 compilation
; optimized code
; optimized using profile data
; rsp based frame
; fully interruptible
; with Dynamic PGO: edge weights are valid, and fgCalledCount is 5628
; 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data G_M000_IG01: ;; offset=0000H
4157 push r15
4156 push r14
57 push rdi
56 push rsi
55 push rbp
53 push rbx
4883EC28 sub rsp, 40
488BF2 mov rsi, rdx G_M000_IG02: ;; offset=000FH
33FF xor edi, edi
488BD9 mov rbx, rcx
33ED xor ebp, ebp
448B7308 mov r14d, dword ptr [rbx+08H]
4585F6 test r14d, r14d
7E27 jle SHORT G_M000_IG05 G_M000_IG03: ;; offset=001FH
8BC5 mov eax, ebp
8B548310 mov edx, dword ptr [rbx+4*rax+10H]
4C8B4618 mov r8, qword ptr [rsi+18H]
48B8A0C2CF3CFC7F0000 mov rax, 0x7FFC3CCFC2A0
4C3BC0 cmp r8, rax
751D jne SHORT G_M000_IG07
446BFA2A imul r15d, edx, 42 G_M000_IG04: ;; offset=003CH
4103FF add edi, r15d
FFC5 inc ebp
443BF5 cmp r14d, ebp
7FD9 jg SHORT G_M000_IG03 G_M000_IG05: ;; offset=0046H
8BC7 mov eax, edi G_M000_IG06: ;; offset=0048H
4883C428 add rsp, 40
5B pop rbx
5D pop rbp
5E pop rsi
5F pop rdi
415E pop r14
415F pop r15
C3 ret G_M000_IG07: ;; offset=0055H
488B4E08 mov rcx, gword ptr [rsi+08H]
41FFD0 call r8
448BF8 mov r15d, eax
EBDB jmp SHORT G_M000_IG04

我选择了i => i * 42中的42常数,以使其在汇编中容易看到,果然,它就在那里。

G_M000_IG03:                ;; offset=001FH
8BC5 mov eax, ebp
8B548310 mov edx, dword ptr [rbx+4*rax+10H]
4C8B4618 mov r8, qword ptr [rsi+18H]
48B8A0C2CF3CFC7F0000 mov rax, 0x7FFC3CCFC2A0
4C3BC0 cmp r8, rax
751D jne SHORT G_M000_IG07
446BFA2A imul r15d, edx, 42

这是从委托中加载目标地址到r8,并加载预期目标的地址到rax。如果它们相同,它就简单地执行内联操作(imul r15d, edx, 42),否则就跳转到G_M000_IG07,调用r8的函数。如果我们把它作为一个基准运行,其效果是显而易见的。

static int[] s_values = Enumerable.Range(0, 1_000).ToArray();

[Benchmark]
public int DelegatePGO() => Sum(s_values, i => i * 42); static int Sum(int[] values, Func<int, int>? func)
{
int sum = 0;
foreach (int value in values)
{
sum += func(value);
}
return sum;
}

在禁用PGO的情况下,我们在.NET 6和.NET 7中得到了相同的性能吞吐量。

方法 运行时间 平均值 比率
DelegatePGO .NET 6.0 1.665 us 1.00
DelegatePGO .NET 7.0 1.659 us 1.00

但当我们启用动态PGO(DOTNET_TieredPGO=1)时,情况发生了变化。.NET 6的速度提高了~14%,但.NET 7的速度提高了~3倍!

方法 运行时间 平均值 比率
DelegatePGO .NET 6.0 1,427.7 ns 1.00
DelegatePGO .NET 7.0 539.0 ns 0.38

dotnet/runtime#70377是动态PGO的另一个有价值的改进,它使PGO能够很好地发挥循环克隆和不变量提升的作用。为了更好地理解这一点,简要地说说这些是什么。循环克隆 (Loop cloning) 是JIT采用的一种机制,以避免循环的快速路径中的各种开销。考虑一下本例中的Test方法。

using System.Runtime.CompilerServices;

class Program
{
static void Main()
{
int[] array = new int[10_000_000];
for (int i = 0; i < 1_000_000; i++)
{
Test(array);
}
} [MethodImpl(MethodImplOptions.NoInlining)]
private static bool Test(int[] array)
{
for (int i = 0; i < 0x12345; i++)
{
if (array[i] == 42)
{
return true;
}
} return false;
}
}

JIT不知道传入的数组是否有足够的长度,以至于在循环中对数组[i]的所有访问都在边界内,因此它需要为每次访问注入边界检查。虽然简单地在前面进行长度检查,并在长度不够的情况下提前抛出一个异常是很好的,但这样做也会改变行为(设想该方法在进行时向数组中写入数据,或者以其他方式改变一些共享状态)。相反,JIT采用了 "循环克隆"。它从本质上重写了这个测试方法,使之更像这样。

if (array is not null && array.Length >= 0x12345)
{
for (int i = 0; i < 0x12345; i++)
{
if (array[i] == 42) // no bounds checks emitted for this access :-)
{
return true;
}
}
}
else
{
for (int i = 0; i < 0x12345; i++)
{
if (array[i] == 42) // bounds checks emitted for this access :-(
{
return true;
}
}
}
return false;

这样一来,以一些代码重复为代价,我们得到了没有边界检查的快速循环,而只需支付慢速路径中的边界检查。你可以在生成的程序集中看到这一点(如果你还不明白,DOTNET_JitDisasm是.NET 7中我最喜欢的功能之一)。

; Assembly listing for method Program:Test(ref):bool
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; Tier-1 compilation
; optimized code
; rsp based frame
; fully interruptible
; No PGO data G_M000_IG01: ;; offset=0000H
4883EC28 sub rsp, 40 G_M000_IG02: ;; offset=0004H
33C0 xor eax, eax
4885C9 test rcx, rcx
7429 je SHORT G_M000_IG05
81790845230100 cmp dword ptr [rcx+08H], 0x12345
7C20 jl SHORT G_M000_IG05
0F1F40000F1F840000000000 align [12 bytes for IG03] G_M000_IG03: ;; offset=0020H
8BD0 mov edx, eax
837C91102A cmp dword ptr [rcx+4*rdx+10H], 42
7429 je SHORT G_M000_IG08
FFC0 inc eax
3D45230100 cmp eax, 0x12345
7CEE jl SHORT G_M000_IG03 G_M000_IG04: ;; offset=0032H
EB17 jmp SHORT G_M000_IG06 G_M000_IG05: ;; offset=0034H
3B4108 cmp eax, dword ptr [rcx+08H]
7323 jae SHORT G_M000_IG10
8BD0 mov edx, eax
837C91102A cmp dword ptr [rcx+4*rdx+10H], 42
7410 je SHORT G_M000_IG08
FFC0 inc eax
3D45230100 cmp eax, 0x12345
7CE9 jl SHORT G_M000_IG05 G_M000_IG06: ;; offset=004BH
33C0 xor eax, eax G_M000_IG07: ;; offset=004DH
4883C428 add rsp, 40
C3 ret G_M000_IG08: ;; offset=0052H
B801000000 mov eax, 1 G_M000_IG09: ;; offset=0057H
4883C428 add rsp, 40
C3 ret G_M000_IG10: ;; offset=005CH
E81FA0C15F call CORINFO_HELP_RNGCHKFAIL
CC int3 ; Total bytes of code 98

G_M000_IG02部分正在进行空值检查和长度检查,如果任何一项失败,则跳转到G_M000_IG05块。如果两者都成功了,它就会执行循环(G_M000_IG03块)而不进行边界检查。

G_M000_IG03:                ;; offset=0020H
8BD0 mov edx, eax
837C91102A cmp dword ptr [rcx+4*rdx+10H], 42
7429 je SHORT G_M000_IG08
FFC0 inc eax
3D45230100 cmp eax, 0x12345
7CEE jl SHORT G_M000_IG03

边界检查只显示在慢速路径块中。

G_M000_IG05:                ;; offset=0034H
3B4108 cmp eax, dword ptr [rcx+08H]
7323 jae SHORT G_M000_IG10
8BD0 mov edx, eax
837C91102A cmp dword ptr [rcx+4*rdx+10H], 42
7410 je SHORT G_M000_IG08
FFC0 inc eax
3D45230100 cmp eax, 0x12345
7CE9 jl SHORT G_M000_IG05

这就是 "循环克隆"。那么,"不变量提升 (invariant hoisting) "呢?提升是指把某个东西从循环中拉到循环之前,而不变量是不会改变的东西。因此,不变量提升是指把某个东西从循环中拉到循环之前,以避免在循环的每个迭代中重新计算一个不会改变的答案。实际上,前面的例子已经展示了不变量提升,即边界检查被移到了循环之前,而不是在循环中,但一个更具体的例子是这样的。

[MethodImpl(MethodImplOptions.NoInlining)]
private static bool Test(int[] array)
{
for (int i = 0; i < 0x12345; i++)
{
if (array[i] == array.Length - 42)
{
return true;
}
} return false;
}

注意,array.Length - 42的值在循环的每次迭代中都不会改变,所以它对循环迭代是 "不变的",可以被抬出来,生成的代码就是这样做的。

G_M000_IG02:                ;; offset=0004H
33D2 xor edx, edx
4885C9 test rcx, rcx
742A je SHORT G_M000_IG05
448B4108 mov r8d, dword ptr [rcx+08H]
4181F845230100 cmp r8d, 0x12345
7C1D jl SHORT G_M000_IG05
4183C0D6 add r8d, -42
0F1F4000 align [4 bytes for IG03] G_M000_IG03: ;; offset=0020H
8BC2 mov eax, edx
4439448110 cmp dword ptr [rcx+4*rax+10H], r8d
7433 je SHORT G_M000_IG08
FFC2 inc edx
81FA45230100 cmp edx, 0x12345
7CED jl SHORT G_M000_IG03

这里我们再次看到数组被测试为空(test rcx, rcx),数组的长度被检查(mov r8d, dword ptr [rcx+08H] then cmp r8d, 0x12345),但是在r8d中有数组的长度,然后我们看到这个前期块从长度中减去42(add r8d, -42),这是在我们继续进入G_M000_IG03块的快速路径循环前。这使得额外的操作集不在循环中,从而避免了每次迭代重新计算数值的开销。

好的,那么这如何适用于动态PGO呢?请记住,对于PGO能够做到的界面/虚拟调度的规避,它是通过进行类型检查,看使用的类型是否是最常见的类型;如果是,它就使用直接调用该类型方法的快速路径(这样做的话,该调用有可能被内联),如果不是,它就回到正常的界面/虚拟调度。这种检查可以不受循环的影响。因此,当一个方法被分层,PGO启动时,类型检查现在可以从循环中提升出来,使得处理普通情况更加便宜。考虑一下我们原来的例子的这个变化。

using System.Runtime.CompilerServices;

class Program
{
static void Main()
{
IPrinter printer = new BlankPrinter();
while (true)
{
DoWork(printer);
}
} [MethodImpl(MethodImplOptions.NoInlining)]
static void DoWork(IPrinter printer)
{
for (int j = 0; j < 123; j++)
{
printer.Print(j);
}
} interface IPrinter
{
void Print(int i);
} class BlankPrinter : IPrinter
{
public void Print(int i)
{
Console.Write("");
}
}
}

当我们看一下在启用动态PGO的情况下为其生成的优化程序集时,我们看到了这个。

; Assembly listing for method Program:DoWork(IPrinter)
; Emitting BLENDED_CODE for X64 CPU with AVX - Windows
; Tier-1 compilation
; optimized code
; optimized using profile data
; rsp based frame
; partially interruptible
; with Dynamic PGO: edge weights are invalid, and fgCalledCount is 12187
; 0 inlinees with PGO data; 1 single block inlinees; 0 inlinees without PGO data G_M000_IG01: ;; offset=0000H
57 push rdi
56 push rsi
4883EC28 sub rsp, 40
488BF1 mov rsi, rcx G_M000_IG02: ;; offset=0009H
33FF xor edi, edi
4885F6 test rsi, rsi
742B je SHORT G_M000_IG05
48B9982DD43CFC7F0000 mov rcx, 0x7FFC3CD42D98
48390E cmp qword ptr [rsi], rcx
751C jne SHORT G_M000_IG05 G_M000_IG03: ;; offset=001FH
48B9282040F948020000 mov rcx, 0x248F9402028
488B09 mov rcx, gword ptr [rcx]
FF1526A80D00 call [Console:Write(String)]
FFC7 inc edi
83FF7B cmp edi, 123
7CE6 jl SHORT G_M000_IG03 G_M000_IG04: ;; offset=0039H
EB29 jmp SHORT G_M000_IG07 G_M000_IG05: ;; offset=003BH
48B9982DD43CFC7F0000 mov rcx, 0x7FFC3CD42D98
48390E cmp qword ptr [rsi], rcx
7521 jne SHORT G_M000_IG08
48B9282040F948020000 mov rcx, 0x248F9402028
488B09 mov rcx, gword ptr [rcx]
FF15FBA70D00 call [Console:Write(String)] G_M000_IG06: ;; offset=005DH
FFC7 inc edi
83FF7B cmp edi, 123
7CD7 jl SHORT G_M000_IG05 G_M000_IG07: ;; offset=0064H
4883C428 add rsp, 40
5E pop rsi
5F pop rdi
C3 ret G_M000_IG08: ;; offset=006BH
488BCE mov rcx, rsi
8BD7 mov edx, edi
49BB1000AA3CFC7F0000 mov r11, 0x7FFC3CAA0010
41FF13 call [r11]IPrinter:Print(int):this
EBDE jmp SHORT G_M000_IG06 ; Total bytes of code 127

我们可以在G_M000_IG02块中看到,它正在对IPrinter实例进行类型检查,如果检查失败就跳到G_M000_IG05(mov rcx, 0x7FFC3CD42D98 then cmp qword ptr [rsi], rcx then jne SHORT G_M000_IG05),否则就跳到G_M000_IG03,这是一个紧密的快速路径循环,内联BlankPrinter.Print,看不到任何类型检查。

有趣的是,这样的改进也会带来自己的挑战。PGO导致了类型检查数量的大幅增加,因为专门针对某一特定类型的调用站点需要与该类型进行比较。然而,普通的子表达式消除 (common subexpression elimination)(CSE)在历史上并不适用这种类型的句柄(CSE是一种编译器优化,通过计算一次结果,然后存储起来供以后使用,而不是每次都重新计算,来消除重复的表达式)。dotnet/runtime#70580通过对这种常量句柄启用CSE来解决这个问题。例如,考虑这个方法。

[Benchmark]
[Arguments("", "", "", "")]
public bool AllAreStrings(object o1, object o2, object o3, object o4) =>
o1 is string && o2 is string && o3 is string && o4 is string;

在.NET 6上,JIT产生了这个汇编代码:

; Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object)
test rdx,rdx
je short M00_L01
mov rax,offset MT_System.String
cmp [rdx],rax
jne short M00_L01
test r8,r8
je short M00_L01
mov rax,offset MT_System.String
cmp [r8],rax
jne short M00_L01
test r9,r9
je short M00_L01
mov rax,offset MT_System.String
cmp [r9],rax
jne short M00_L01
mov rax,[rsp+28]
test rax,rax
je short M00_L00
mov rdx,offset MT_System.String
cmp [rax],rdx
je short M00_L00
xor eax,eax
M00_L00:
test rax,rax
setne al
movzx eax,al
ret
M00_L01:
xor eax,eax
ret
; Total bytes of code 100

请注意,C#对字符串有四个测试,而汇编代码中的mov rax,offset MT_System.String有四个加载。现在在.NET 7上,加载只执行一次。

; Program.AllAreStrings(System.Object, System.Object, System.Object, System.Object)
test rdx,rdx
je short M00_L01
mov rax,offset MT_System.String
cmp [rdx],rax
jne short M00_L01
test r8,r8
je short M00_L01
cmp [r8],rax
jne short M00_L01
test r9,r9
je short M00_L01
cmp [r9],rax
jne short M00_L01
mov rdx,[rsp+28]
test rdx,rdx
je short M00_L00
cmp [rdx],rax
je short M00_L00
xor edx,edx
M00_L00:
xor eax,eax
test rdx,rdx
setne al
ret
M00_L01:
xor eax,eax
ret
; Total bytes of code 69

原文链接

Performance Improvements in .NET 7

本作品采用知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议进行许可。

欢迎转载、使用、重新发布,但务必保留文章署名 郑子铭 (包含链接: http://www.cnblogs.com/MingsonZheng/ ),不得用于商业目的,基于本文修改后的作品务必以相同的许可发布。

如有任何疑问,请与我联系 (MingsonZheng@outlook.com)

【译】.NET 7 中的性能改进(三)的更多相关文章

  1. 【译】ASP.NET Core 6 中的性能改进

    原文 | Brennan Conroy 翻译 | 郑子铭 受到 Stephen Toub 关于 .NET 性能的博文的启发,我们正在写一篇类似的文章来强调 6.0 中对 ASP.NET Core 所做 ...

  2. 【翻译】.NET 5中的性能改进

    [翻译].NET 5中的性能改进 在.NET Core之前的版本中,其实已经在博客中介绍了在该版本中发现的重大性能改进. 从.NET Core 2.0到.NET Core 2.1到.NET Core ...

  3. .NET 4.6中的性能改进

    .NET 4.6中带来了一些与性能改进相关的CLR特性,这些特性中有一部分将会自动生效,而另外一些特性,例如SIMD与异步本地存储(Async Local Storage)则需要对编写应用的方式进行某 ...

  4. .NET性能系列文章一:.NET7的性能改进

    这些方法在.NET7中变得更快 照片来自 CHUTTERSNAP 的 Unsplash 欢迎阅读.NET性能系列的第一章.这一系列的特点是对.NET世界中许多不同的主题进行研究.比较性能.正如标题所说 ...

  5. .NET 5 中的正则引擎性能改进(翻译)

    前言 System.Text.RegularExpressions 命名空间已经在 .NET 中使用了多年,一直追溯到 .NET Framework 1.1.它在 .NET 实施本身的数百个位置中使用 ...

  6. 译<容器网络中OVS-DPDK的性能>

    译<容器网络中OVS-DPDK的性能> 本文来自对Performance of OVS-DPDK in Container Networks的翻译. 概要--网络功能虚拟化(Network ...

  7. KlayGE 4.4中渲染的改进(三):高质量无限地形

    转载请注明出处为KlayGE游戏引擎,本文的永久链接为http://www.klayge.org/?p=2761   本系列的上一篇讲了DR中的一些改进.本篇开始将描述这个版本加入的新功能,高质量地形 ...

  8. [译]async/await中使用阻塞式代码导致死锁 百万数据排序:优化的选择排序(堆排序)

    [译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Cleary的 ...

  9. Lazy<T>在Entity Framework中的性能优化实践

    Lazy<T>在Entity Framework中的性能优化实践(附源码) 2013-10-27 18:12 by JustRun, 328 阅读, 4 评论, 收藏, 编辑 在使用EF的 ...

  10. [译]async/await中使用阻塞式代码导致死锁

    原文:[译]async/await中使用阻塞式代码导致死锁 这篇博文主要是讲解在async/await中使用阻塞式代码导致死锁的问题,以及如何避免出现这种死锁.内容主要是从作者Stephen Clea ...

随机推荐

  1. java:绘制图形

    java绘图类:Graphics类 绘图是高级程序中必备的技术,在很多方面都能用到,如:绘制闪屏图片,背景图片和组件外观等. 1.Graphics类 Graphics类是所有图形上下文的抽象基类,Gr ...

  2. fbterm的配置,纯文本终端显示中文

    安装 fbterm sudo apt-get install fbterm 设置普通用户可以执行 fbterm 命令 sudo adduser username video #username为用户名 ...

  3. C++ 之 宏定义

    宏在 C 语言中非常重要,但在 C++ 中却无甚大用,普遍的共识:尽量避免使用宏 C++ 之父 Bjarne 在<C++ Programming Language>中写到 Avoid ma ...

  4. 在实际应用中联合体union的妙用

    关键字union,又称为联合体.共用体,联合体的声明和结构体类似,但是它的行为方式又和结构体不同,这里的行为方式主要指的是其在内存中的体现,结构体中的成员每一个占据不同的内存空间,而联合体中的所有成员 ...

  5. (小白向)2020-12-18 中国大学MOOC第十二讲-动态变量应用

    1创建单向链表(10分) 问题描述:根据随机输入的若干非零整数,以数字0结束:建立一个新链表. 输入:随机输入若干个整数,以数字0结束 输出:新建链表中个节点的值,数字间没有间隔字符. 样例:输入 5 ...

  6. 【Java难点攻克】「NIO和内存映射性能提升系列」彻底透析NIO底层的内存映射机制原理与Direct Memory的关系

    NIO与内存映射文件 Java类库中的NIO包相对于IO包来说有一个新功能就是 [内存映射文件],在业务层面的日常开发过程中并不是经常会使用,但是一旦在处理大文件时是比较理想的提高效率的手段,之前已经 ...

  7. 七个步骤覆盖 API 接口测试

    接口测试作为最常用的集成测试方法的一部分,通过直接调用被测试的接口来确定系统在功能性.可靠性.安全性和性能方面是否能达到预期,有些情况是功能测试无法覆盖的,所以接口测试是非常必要的.首先需要对接口测试 ...

  8. 图书管理系统、聚合函数、分组查询、F与Q查询

    目录 图书管理系统 1.表设计 2.首页搭建.展示 书籍的添加 书籍编辑 书籍删除 聚合函数 Max Min Sum Count Avg 分组查询 按照表分组 按照字段分组 F与Q查询 F查询 Q查询 ...

  9. 【转载】ADOX.Catalog中文帮助详细说明chm文档

    首先给个完全版的地址,如果您机器上装过OFFICE应该可以打开的:ADOX 对象模型, 地址是:"C:\Program Files\Common Files\Microsoft Shared ...

  10. 【c#】分享一个简易的基于时间轮调度的延迟任务实现

    在很多.net开发体系中开发者在面对调度作业需求的时候一般会选择三方开源成熟的作业调度框架来满足业务需求,比如Hangfire.Quartz.NET这样的框架.但是有些时候可能我们只是需要一个简易的延 ...