很久没有写过 .NET Core 相关的文章了,目前关店在家休息所以有些时间写一篇新的。这次的文章主要介绍如何在 Linux 上编译调试最新的 .NET Core 5.0 Preview 与简单分析 Span 的实现原理。微软从 .NET Core 5.0 开始把 GIT 仓库 coreclr 与 corefx 合并移动到了 runtime 仓库,原有仓库仅用于维护 .NET Core 3.x,你可以从以下地址查看最新的源代码:

https://github.com/dotnet/runtime

为了方便重现,接下来的编译调试会使用 docker 与 ubuntu 18.04 镜像(尽管微软提供了编译专用的镜像但并不适合调试分析),步骤会与之前的博客介绍的 1.1,书籍介绍的 2.1 有一些不同。

如果你觉得阅读这篇文章有困难,可以参考我之前发布的 .NET Core 源代码分析系列或者书籍《.NET Core 底层入门》,书籍的购买链接在文章最后。

编译 .NET Core 5.0 Preview

本文编译的版本是 0d607a757372e3ecc8e942141d7f586a98694e42

创建 docker 容器

执行以下命令即可创建一个 ubuntu 18.04 的 docker 容器,注意创建时需要使用 --privileged 参数,否则无法使用 lldb 或者 gdb 调试程序。

  1. docker run -it --privileged ubuntu:18.04

安装 cmake

.NET Core 5.0 要求的 cmake 版本非常高,我们需要添加第三方源来安装新版本的 cmake:

  1. apt-get update
  2. apt-get install apt-transport-https ca-certificates gnupg software-properties-common
  3. wget -O - https://apt.kitware.com/keys/kitware-archive-latest.asc 2>/dev/null | apt-key add -
  4. apt-add-repository 'deb https://apt.kitware.com/ubuntu/ bionic main'
  5. apt-get update

安装依赖的类库与工具

这个步骤与之前版本的 .NET Core 相同:

  1. apt-get install git wget locales locales-all vim
  2. apt-get install cmake llvm-3.9 clang-9 libunwind8 libunwind8-dev gettext libicu-dev liblttng-ust-dev libcurl4-openssl-dev libssl-dev libnuma-dev libkrb5-dev

下载 .NET Core 源代码并编译

这个步骤也与之前的 .NET Core 相同,但因为 corefx 合并到了同一个仓库中,执行以下步骤以后会同时编译 corefx 的 dll 文件。注意这个步骤编译的是 Debug 版本的运行时,方便后面的调试。

  1. git clone https://github.com/dotnet/runtime
  2. cd runtime
  3. ./build.sh

编译完成后你可以在 artifacts 文件夹下找到编译结果。

使用 .NET Core 5.0 Preview 执行 Hello World 程序

接下来我们会看如何使用自己编译的 .NET Core 执行一个 Hello World 程序,.NET Core 5.0 会同时编译出 dotnet 程序,我们可以使用它代替 corerun 来简化运行步骤(不需要像以前的版本一样手动复制 corefx 的 dll或者设置 CORE_ROOT 环境变量)。但因为 runtime 仓库中不包括 sdk(sdk 在 sdk 仓库中,这次懒得编译),我们仍然需要另外安装一个官方的 .NET Core 用于创建与编译 Hello World 程序。

安装官方的 .NET Core 3.1 SDK

  1. wget -q https://packages.microsoft.com/config/ubuntu/19.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb
  2. dpkg -i packages-microsoft-prod.deb
  3. apt-get update
  4. apt-get install dotnet-sdk-3.1

创建与编译 Hello World 程序

  1. mkdir /console
  2. cd /console
  3. dotnet new console
  4. dotnet build

执行 Hello World 程序

因为使用了 .NET Core 3.1 的 SDK 编译,我们还需要修改 程序名.runtimeconfig.json 中的运行时版本号,否则会出现版本号不一致而执行失败的问题。

  1. cd /console/bin/Debug/netcoreapp3.1
  2. vi console.runtimeconfig.json

需要修改两处:

  • runtimeOptions.tfm 修改到 netcoreapp5.0
  • runtimeOptions.framework.version 修改到 5.0.0

修改完以后使用以下命令即可执行:

  1. /runtime/artifacts/bin/testhost/netcoreapp5.0-Linux-Debug-x64/dotnet console.dll

如果看到 Hello World 输出就代表执行成功了。

调试 .NET Core 5.0 Preview

在 linux 上调试 .NET Core 一般使用 lldb (gdb 也可以但是没有 SOS 插件支持),SOS 插件的源代码被搬到了 diagnostics 仓库,所以我们还需要下载编译这个仓库的源代码。

下载编译 diagnostics 仓库 (LLDB SOS 插件)

安装 LLDB 与 LLDB 的开发文件:

  1. apt-get install clang llvm lldb liblldb-3.9-dev

下载编译 diagnostics 仓库:

  1. git clone https://github.com/dotnet/diagnostics
  2. cd diagnostics
  3. ./build.sh

编译成功后你可以在 /diagnostics/artifacts/bin/Linux.x64.Debug/libsosplugin.so 找到 SOS 插件的 dll 文件。

使用 LLDB 调试 .NET Core

SOS 插件需要在执行到达 LoadLibraryExW 后才可以正常使用,使用 LLDB 的 -o 参数可以省略每次调试的时候都要做的准备工作:

  1. cd /console/bin/Debug/netcoreapp3.1
  2. lldb \
  3. -o "plugin load /diagnostics/artifacts/bin/Linux.x64.Debug/libsosplugin.so" \
  4. -o "process launch -s" \
  5. -o "process handle -s false SIGUSR1 SIGUSR2" \
  6. -o "b LoadLibraryExW" \
  7. -o "c" \
  8. -o "br del 1" \
  9. -o "sos Help" \
  10. /runtime/artifacts/bin/testhost/netcoreapp5.0-Linux-Debug-x64/dotnet console.dll

执行以后会停在 LoadLibraryExW 并打印出 SOS 插件的帮助,接下来我们可以使用 SOS 插件给托管函数下断点:

  1. sos bpmd console.dll console.Program.Main

然后使用 c 命令继续执行程序,直到触发断点:

  1. c

到达断点(JIT 编译后的托管函数 Main)以后我们可以使用 SOS 插件打印这个托管函数编译出来的汇编内容:

  1. sos u $rip

如果到此都没有问题,那么接下来我们可以开始分析 Span 的实现原理了。

Span 与 Memory 简介

Span 与 Memory 是微软推出的,用于表示某段子内容的数据类型,它们的主要目的是为了减少内存分配与复制,例如取 "abcdefg" 的子字符串 "def",传统的方法 (Substring) 会分配一个长度为 3 的新字符串然后复制 "def" 过去,但 Span 与 Memory 可以直接使用原有的对象、子内容的开始位置与子内容的长度来表示一段子内容。在其他语言中也有类似 Span 与 Memory 的概念,例如 go 中的 slice,c 中指针与长度的结合 (例如 struct char_view { char* ptr, size_t size; }),与 c++ 中的 string_viewspan 类型。

Span 与 Memory 的区别在于,Memory 是一个普通的类型,只保存 原有的对象子内容的开始地址子内容的长度,在内存中的表现可以参考下图:

Memory 与很早就存在的 ArraySegment 实质上是一样的,只是支持更多的类型,它们都不需要运行时或者编译器的额外支持。

Span 则特殊很多,它保存了子内容的开始地址与长度(不保存原始对象的地址),使得它不需要计算开始地址并且允许指向托管对象以外的内容 (例如从 stackalloc 分配)。Span 在内存中的表现可以参考下图:

Span 是一个 ref struct 类型 (这个类型可以说是专门为 Span 发明的),ref struct 只能保存在于栈上或者作为其他 ref struct 的成员 (最终来说只能保存在于栈上),Span 只能存在于栈上主要有以下原因:

  • GC 处理 Span 对象的成本很高,所以不应该大范围使用
  • Span 的读写是非原子的(两个指针大小),如果允许在堆上就有可能被多个线程同时访问
  • Span 可以由 stackalloc 生成,而 Span 自身并不会标记来源是托管对象还是栈空间

因为 Span 需要运行时的额外支持,在 .NET Framework 与 Mono 上使用的 Span (从 Nuget 包安装的) 实际上与 Memory 一样,只有在 .Net Core 上才有以上的特性。

此外,因为部分对象的内容不可修改 (例如 string),所以还有配套的 ReadOnlySpanReadOnlyMemory,它们除了在编译器层面上限制修改以外,与原类型没有什么区别。

调试分析 Span 的实现原理

接下来我们可以调试一个示例程序,简单分析 Span 在运行时中的实现原理 (这次分析不涉及到 JIT 部分,虽然 JIT 部分很少)。

以下是示例程序的代码:

  1. using System;
  2. namespace console
  3. {
  4. class Program
  5. {
  6. static void Main(string[] args)
  7. {
  8. Span<byte> span = new byte[10] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
  9. span = span.Slice(5, 2);
  10. GC.Collect();
  11. Console.WriteLine(span.Length);
  12. }
  13. }
  14. }

使用 LLDB 查看生成的汇编代码

编译示例程序与执行 LLDB 的命令请参考前面的内容,执行后可以使用以下命令给托管函数 Main 下断点然后执行到断点,并查看汇编代码:

  1. sos bpmd console.dll console.Program.Main
  2. c
  3. sos u $rip

输出如下:

  1. (lldb) sos bpmd console.dll console.Program.Main
  2. Adding pending breakpoints...
  3. (lldb) c
  4. Process 6460 resuming
  5. JITTED console!console.Program.Main(System.String[])
  6. Setting breakpoint: breakpoint set --address 0x00007FFF7BB352D0 [console.Program.Main(System.String[])]
  7. Process 6460 stopped
  8. * thread #1, name = 'dotnet', stop reason = breakpoint 3.1
  9. frame #0: 0x00007fff7bb352d0
  10. -> 0x7fff7bb352d0: pushq %rbp
  11. 0x7fff7bb352d1: pushq %r13
  12. 0x7fff7bb352d3: subq $0x48, %rsp
  13. 0x7fff7bb352d7: vzeroupper
  14. (lldb) sos u $rip
  15. Normal JIT generated code
  16. console.Program.Main(System.String[])
  17. ilAddr is 00007FFFF18BB250 pImport is 00005576894771F0
  18. Begin 00007FFF7BB352D0, size bc
  19. /console/Program.cs @ 9:
  20. >>> 00007fff7bb352d0 55 push rbp
  21. 00007fff7bb352d1 4155 push r13
  22. 00007fff7bb352d3 4883ec48 sub rsp, 0x48
  23. 00007fff7bb352d7 c5f877 vzeroupper
  24. 00007fff7bb352da 488d6c2450 lea rbp, [rsp + 0x50]
  25. 00007fff7bb352df 4c8bef mov r13, rdi
  26. 00007fff7bb352e2 488d7db0 lea rdi, [rbp - 0x50]
  27. 00007fff7bb352e6 b910000000 mov ecx, 0x10
  28. 00007fff7bb352eb 33c0 xor eax, eax
  29. 00007fff7bb352ed f3ab rep stosd dword ptr es:[rdi], eax
  30. 00007fff7bb352ef 498bfd mov rdi, r13
  31. 00007fff7bb352f2 48897df0 mov qword ptr [rbp - 0x10], rdi
  32. 00007fff7bb352f6 48bfe05fd87bff7f0000 movabs rdi, 0x7fff7bd85fe0
  33. 00007fff7bb35300 be0a000000 mov esi, 0xa
  34. 00007fff7bb35305 e8063fe079 call 0x7ffff5939210 (JitHelp: CORINFO_HELP_NEWARR_1_VC)
  35. 00007fff7bb3530a 488945d8 mov qword ptr [rbp - 0x28], rax
  36. 00007fff7bb3530e 48bf2894e07bff7f0000 movabs rdi, 0x7fff7be09428
  37. 00007fff7bb35318 e8b396e079 call 0x7ffff593e9d0 (JitHelp: CORINFO_HELP_FIELDDESC_TO_STUBRUNTIMEFIELD)
  38. 00007fff7bb3531d 488945d0 mov qword ptr [rbp - 0x30], rax
  39. 00007fff7bb35321 488b7dd8 mov rdi, qword ptr [rbp - 0x28]
  40. 00007fff7bb35325 488b75d0 mov rsi, qword ptr [rbp - 0x30]
  41. 00007fff7bb35329 e8829f307a call 0x7ffff5e3f2b0 (System.Runtime.CompilerServices.RuntimeHelpers.InitializeArray(System.Array, System.RuntimeFieldHandle), mdToken: 0000000006003730)
  42. 00007fff7bb3532e 488b7dd8 mov rdi, qword ptr [rbp - 0x28]
  43. 00007fff7bb35332 e8f9ecffff call 0x7fff7bb34030 (System.Span`1[[System.Byte, System.Private.CoreLib]].op_Implicit(Byte[]), mdToken: 00000000060012B1)
  44. 00007fff7bb35337 488945c0 mov qword ptr [rbp - 0x40], rax
  45. 00007fff7bb3533b 488955c8 mov qword ptr [rbp - 0x38], rdx
  46. 00007fff7bb3533f c5fa6f45c0 vmovdqu xmm0, xmmword ptr [rbp - 0x40]
  47. 00007fff7bb35344 c5fa7f45e0 vmovdqu xmmword ptr [rbp - 0x20], xmm0
  48. /console/Program.cs @ 10:
  49. 00007fff7bb35349 488d7de0 lea rdi, [rbp - 0x20]
  50. 00007fff7bb3534d be05000000 mov esi, 0x5
  51. 00007fff7bb35352 ba02000000 mov edx, 0x2
  52. 00007fff7bb35357 e844edffff call 0x7fff7bb340a0 (System.Span`1[[System.Byte, System.Private.CoreLib]].Slice(Int32, Int32), mdToken: 00000000060012BE)
  53. 00007fff7bb3535c 488945b0 mov qword ptr [rbp - 0x50], rax
  54. 00007fff7bb35360 488955b8 mov qword ptr [rbp - 0x48], rdx
  55. 00007fff7bb35364 c5fa6f45b0 vmovdqu xmm0, xmmword ptr [rbp - 0x50]
  56. 00007fff7bb35369 c5fa7f45e0 vmovdqu xmmword ptr [rbp - 0x20], xmm0
  57. /console/Program.cs @ 11:
  58. 00007fff7bb3536e e845b3ffff call 0x7fff7bb306b8 (System.GC.Collect(), mdToken: 0000000006000361)
  59. /console/Program.cs @ 12:
  60. 00007fff7bb35373 488d7de0 lea rdi, [rbp - 0x20]
  61. 00007fff7bb35377 e87cecffff call 0x7fff7bb33ff8 (System.Span`1[[System.Byte, System.Private.CoreLib]].get_Length(), mdToken: 00000000060012AC)
  62. 00007fff7bb3537c 8bf8 mov edi, eax
  63. 00007fff7bb3537e e8a5fcffff call 0x7fff7bb35028 (System.Console.WriteLine(Int32), mdToken: 0000000006000089)
  64. /console/Program.cs @ 13:
  65. 00007fff7bb35383 90 nop
  66. 00007fff7bb35384 488d65f8 lea rsp, [rbp - 0x8]
  67. 00007fff7bb35388 415d pop r13
  68. 00007fff7bb3538a 5d pop rbp
  69. 00007fff7bb3538b c3 ret

我们可以看到 00007fff7bb35305 处的指令从托管堆分配了数组,00007fff7bb35329 处的指令初始化了数组内容,00007fff7bb35332 处的指令生成了第一个 span 对象,00007fff7bb35357 处的指令生成了第二个 span 对象。你可以从每一段汇编代码上标记的文件名与行数找到对应的 C# 代码。

分析栈上的内容

接下来我们会分析栈上的内容,包括数组的地址与 span 的内容等。

注意栈上会保存临时变量和不使用的参数,这是因为之前的编译没有使用 Release 配置,你可以使用 Release 配置编译再按这里的步骤试试有什么不同 (可能会更难理解一些),使用 Release 配置时请关闭分层编译,使用 export COMPlus_TieredCompilation=0 即可关闭。

首先我们来看看分配数组之前栈上 (当前帧) 有什么内容:

  1. (lldb) b 0x00007fff7bb35305
  2. Breakpoint 4: address = 0x00007fff7bb35305 # 分配数组的指令
  3. (lldb) c
  4. Process 6460 resuming
  5. Process 6460 stopped
  6. * thread #1, name = 'dotnet', stop reason = breakpoint 4.1
  7. frame #0: 0x00007fff7bb35305
  8. -> 0x7fff7bb35305: callq 0x7ffff5939210 ; JIT_NewArr1VC_MP_FastPortable at jithelpers.cpp:2560
  9. 0x7fff7bb3530a: movq %rax, -0x28(%rbp)
  10. 0x7fff7bb3530e: movabsq $0x7fff7be09428, %rdi ; imm = 0x7FFF7BE09428
  11. 0x7fff7bb35318: callq 0x7ffff593e9d0 ; JIT_GetRuntimeFieldStub at jithelpers.cpp:3635
  12. (lldb) p/x $rsp
  13. (unsigned long) $2 = 0x00007fffffffd220 # 栈顶
  14. (lldb) p/x $rbp
  15. (unsigned long) $3 = 0x00007fffffffd270 # 帧底
  16. (lldb) p $rbp - $rsp
  17. (unsigned long) $4 = 80 # 当前帧大小
  18. (lldb) memory read -s 1 -c 80 0x00007fffffffd220
  19. 0x7fffffffd220: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ # 本地变量使用的空间
  20. 0x7fffffffd230: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ # 本地变量使用的空间
  21. 0x7fffffffd240: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ # 本地变量使用的空间
  22. 0x7fffffffd250: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................ # 本地变量使用的空间
  23. 0x7fffffffd260: b0 d5 00 54 ff 7f 00 00 00 00 00 00 00 00 00 00 ...T............ # rbp-0x10 是 args 参数,rbp-0x8 是上一帧 r13 的值

接下来我们看看原始数组的地址与数组的内容,数组的本地变量 (临时变量) 会保存到 $rbp-0x28,我们可以直接看这个地址中的内容。

  1. (lldb) b 0x00007fff7bb3532e
  2. Breakpoint 5: address = 0x00007fff7bb3532e # 初始化数组后的指令
  3. (lldb) c
  4. Process 6460 resuming
  5. Process 6460 stopped
  6. * thread #1, name = 'dotnet', stop reason = breakpoint 5.1
  7. frame #0: 0x00007fff7bb3532e
  8. -> 0x7fff7bb3532e: movq -0x28(%rbp), %rdi
  9. 0x7fff7bb35332: callq 0x7fff7bb34030
  10. 0x7fff7bb35337: movq %rax, -0x40(%rbp)
  11. 0x7fff7bb3533b: movq %rdx, -0x38(%rbp)
  12. (lldb) p/x $rbp-0x28
  13. (unsigned long) $6 = 0x00007fffffffd248
  14. (lldb) memory read -s 1 -c 8 0x00007fffffffd248
  15. 0x7fffffffd248: 70 ed 00 54 ff 7f 00 00 p..T....
  16. (lldb) dumpobj 7fff5400ed70 # SOS 插件提供的命令,用于输出托管对象信息
  17. Name: System.Byte[]
  18. MethodTable: 00007fff7bd85fe0
  19. EEClass: 00007fff7bd85f30
  20. Size: 34(0x22) bytes
  21. Array: Rank 1, Number of elements 10, Type Byte
  22. Content: ..........
  23. Fields:
  24. None
  25. (lldb) memory read -s 1 -c 26 0x7fff5400ed70 # 显示数组对象的内容
  26. 0x7fff5400ed70: e0 5f d8 7b ff 7f 00 00 0a 00 00 00 00 00 00 00 ._.{............ # 0~8 是类型信息,8~16 是长度
  27. 0x7fff5400ed80: 01 02 03 04 05 06 07 08 09 0a .......... # 16~26 是数组内容

接下来我们可以继续执行,然后看看各个 Span 的内容:

  1. (lldb) b 0x00007fff7bb3536e
  2. Breakpoint 6: address = 0x00007fff7bb3536e
  3. (lldb) c
  4. Process 6460 resuming
  5. Process 6460 stopped
  6. * thread #1, name = 'dotnet', stop reason = breakpoint 6.1
  7. frame #0: 0x00007fff7bb3536e
  8. -> 0x7fff7bb3536e: callq 0x7fff7bb306b8
  9. 0x7fff7bb35373: leaq -0x20(%rbp), %rdi
  10. 0x7fff7bb35377: callq 0x7fff7bb33ff8
  11. 0x7fff7bb3537c: movl %eax, %edi
  12. (lldb) memory read -s 1 -c 16 $rbp-0x40
  13. 0x7fffffffd230: 80 ed 00 54 ff 7f 00 00 0a 00 00 00 00 00 00 00 ...T............ # 第一个 span (临时变量) 的开始地址与长度
  14. (lldb) memory read -s 1 -c 16 $rbp-0x50
  15. 0x7fffffffd220: 85 ed 00 54 ff 7f 00 00 02 00 00 00 00 00 00 00 ...T............ # 第二个 span (临时变量) 的开始地址与长度
  16. (lldb) memory read -s 1 -c 16 $rbp-0x20
  17. 0x7fffffffd250: 85 ed 00 54 ff 7f 00 00 02 00 00 00 00 00 00 00 ...T............ # 本地变量 span 中的开始地址与长度

从输出中我们可以看到,第一个 span 的地址是 0x7fff5400ed80,这刚好是数组地址 0x7fff5400ed70 加上类型信息 (8) 与长度 (8) 以后的值,
也就是数组的内容,使用以下命令可以查看这个 span 指向的内容:

  1. (lldb) memory read -s 1 -c 10 0x7fff5400ed80
  2. 0x7fff5400ed80: 01 02 03 04 05 06 07 08 09 0a ..........

而第二个 span 的地址 0x7fff5400ed85 则是第一个 span 的地址加 5,并且长度为 2,使用以下命令可以查看这个 span 指向的内容:

  1. (lldb) memory read -s 1 -c 2 0x7fff5400ed85
  2. 0x7fff5400ed85: 06 07 ..

最后再看看栈上 (当前帧) 的内容:

  1. (lldb) memory read -s 1 -c 80 0x00007fffffffd220
  2. 0x7fffffffd220: 85 ed 00 54 ff 7f 00 00 02 00 00 00 00 00 00 00 ...T............ # 本地变量 span 中的开始地址与长度
  3. 0x7fffffffd230: 80 ed 00 54 ff 7f 00 00 0a 00 00 00 00 00 00 00 ...T............ # 第一个 span (临时变量) 的开始地址与长度
  4. 0x7fffffffd240: 98 ed 00 54 ff 7f 00 00 70 ed 00 54 ff 7f 00 00 ...T....p..T.... # 用于初始化数组的句柄,原始数组对象 (临时变量)
  5. 0x7fffffffd250: 85 ed 00 54 ff 7f 00 00 02 00 00 00 00 00 00 00 ...T............ # 第二个 span (临时变量) 的开始地址与长度
  6. 0x7fffffffd260: b0 d5 00 54 ff 7f 00 00 00 00 00 00 00 00 00 00 ...T............ # args 参数与上一帧 r13 的值

查看托管函数对应 GC 信息中的各个 Slot

GC 信息是 .NET 运行时查找各个线程中托管函数的本地变量 (根对象) 时使用的信息,因为 GC 信息的编码非常复杂,这里不会介绍如何解码 GC 信息,
而是下断点来看各个 Slot 的内容,从扫描到标记的调用链跟踪 (backtrace) 如下:

  1. * frame #0: 0x00007ffff5cb0fcf libcoreclr.so`WKS::gc_heap::mark_object_simple(po=0x00007fffffffa460) at gc.cpp:19675
  2. frame #1: 0x00007ffff5cb6fe8 libcoreclr.so`WKS::GCHeap::Promote(ppObject=0x00007fffffffd230, sc=0x00007fffffffc9c0, flags=1) at gc.cpp:36730
  3. frame #2: 0x00007ffff5808fe8 libcoreclr.so`PromoteCarefully(fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), ppObj=0x00007fffffffd230, sc=0x00007fffffffc9c0, flags=1)(Object**, ScanContext*, unsigned int), Object**, ScanContext*, unsigned int) at siginfo.cpp:4874
  4. frame #3: 0x00007ffff5918c4a libcoreclr.so`GcEnumObject(pData=0x00007fffffffc710, pObj=0x00007fffffffd230, flags=1) at gcenv.ee.common.cpp:167
  5. frame #4: 0x00007ffff5a87abc libcoreclr.so`GcInfoDecoder::ReportStackSlotToGC(this=0x00007fffffffab38, spOffset=-80, spBase=GC_FRAMEREG_REL, gcFlags=1, pRD=0x00007fffffffb5c0, flags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.cpp:1848
  6. frame #5: 0x00007ffff5a88381 libcoreclr.so`GcInfoDecoder::ReportSlotToGC(this=0x00007fffffffab38, slotDecoder=0x00007fffffffa8d0, slotIndex=0, pRD=0x00007fffffffb5c0, reportScratchSlots=true, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.h:679
  7. frame #6: 0x00007ffff5a8666d libcoreclr.so`GcInfoDecoder::ReportUntrackedSlots(this=0x00007fffffffab38, slotDecoder=0x00007fffffffa8d0, pRD=0x00007fffffffb5c0, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.cpp:1034
  8. frame #7: 0x00007ffff5a85d28 libcoreclr.so`GcInfoDecoder::EnumerateLiveSlots(this=0x00007fffffffab38, pRD=0x00007fffffffb5c0, reportScratchSlots=false, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.cpp:983
  9. frame #8: 0x00007ffff570225a libcoreclr.so`EECodeManager::EnumGcRefs(this=0x0000555555822680, pRD=0x00007fffffffb5c0, pCodeInfo=0x00007fffffffb3f0, flags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc710, relOffsetOverride=4294967295)(void*, OBJECTREF*, unsigned int), void*, unsigned int) at eetwain.cpp:5150
  10. frame #9: 0x00007ffff5919462 libcoreclr.so`GcStackCrawlCallBack(pCF=0x00007fffffffb1c0, pData=0x00007fffffffc710) at gcenv.ee.common.cpp:283
  11. frame #10: 0x00007ffff580e52f libcoreclr.so`Thread::MakeStackwalkerCallback(this=0x0000555555838aa0, pCF=0x00007fffffffb1c0, pCallback=(libcoreclr.so`GcStackCrawlCallBack(CrawlFrame*, void*) at gcenv.ee.common.cpp:201), pData=0x00007fffffffc710, uFramesProcessed=5)(CrawlFrame*, void*), void*, unsigned int) at stackwalk.cpp:886
  12. frame #11: 0x00007ffff580e77b libcoreclr.so`Thread::StackWalkFramesEx(this=0x0000555555838aa0, pRD=0x00007fffffffb5c0, pCallback=(libcoreclr.so`GcStackCrawlCallBack(CrawlFrame*, void*) at gcenv.ee.common.cpp:201), pData=0x00007fffffffc710, flags=34048, pStartFrame=0x0000000000000000)(CrawlFrame*, void*), void*, unsigned int, Frame*) at stackwalk.cpp:966
  13. frame #12: 0x00007ffff580f337 libcoreclr.so`Thread::StackWalkFrames(this=0x0000555555838aa0, pCallback=(libcoreclr.so`GcStackCrawlCallBack(CrawlFrame*, void*) at gcenv.ee.common.cpp:201), pData=0x00007fffffffc710, flags=34048, pStartFrame=0x0000000000000000)(CrawlFrame*, void*), void*, unsigned int, Frame*) at stackwalk.cpp:1049
  14. frame #13: 0x00007ffff5ceeadb libcoreclr.so`ScanStackRoots(pThread=0x0000555555838aa0, fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), sc=0x00007fffffffc9c0)(Object**, ScanContext*, unsigned int), ScanContext*) at gcenv.ee.cpp:146
  15. frame #14: 0x00007ffff5cee7ab libcoreclr.so`GCToEEInterface::GcScanRoots(fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), condemned=2, max_gen=2, sc=0x00007fffffffc9c0)(Object**, ScanContext*, unsigned int), int, int, ScanContext*) at gcenv.ee.cpp:182
  16. frame #15: 0x00007ffff5cfa3d9 libcoreclr.so`GCScan::GcScanRoots(fn=(libcoreclr.so`WKS::GCHeap::Promote(Object**, ScanContext*, unsigned int) at gc.cpp:36666), condemned=2, max_gen=2, sc=0x00007fffffffc9c0)(Object**, ScanContext*, unsigned int), int, int, ScanContext*) at gcscan.cpp:155
  17. frame #16: 0x00007ffff5c9f701 libcoreclr.so`WKS::gc_heap::mark_phase(condemned_gen_number=2, mark_only_p=NO) at gc.cpp:21062
  18. frame #17: 0x00007ffff5c9b479 libcoreclr.so`WKS::gc_heap::gc1() at gc.cpp:16713
  19. frame #18: 0x00007ffff5cab832 libcoreclr.so`WKS::gc_heap::garbage_collect(n=2) at gc.cpp:18345
  20. frame #19: 0x00007ffff5c90dea libcoreclr.so`WKS::GCHeap::GarbageCollectGeneration(this=0x0000555555793aa0, gen=2, reason=reason_induced) at gc.cpp:38188
  21. frame #20: 0x00007ffff5cdd3bb libcoreclr.so`WKS::GCHeap::GarbageCollectTry(this=0x0000555555793aa0, generation=2, low_memory_p=NO, mode=2) at gc.cpp:37524
  22. frame #21: 0x00007ffff5cde614 libcoreclr.so`WKS::GCHeap::GarbageCollect(this=0x0000555555793aa0, generation=2, low_memory_p=false, mode=2) at gc.cpp:37458
  23. frame #22: 0x00007ffff58be151 libcoreclr.so`GCInterface::Collect(generation=-1, mode=2) at comutilnative.cpp:986
  24. frame #23: 0x00007fff7bb55853
  25. frame #24: 0x00007fff7bb55788
  26. frame #25: 0x00007fff7bb553c3
  27. frame #26: 0x00007ffff5a965f3 libcoreclr.so`CallDescrWorkerInternal at unixasmmacrosamd64.inc:862
  28. frame #27: 0x00007ffff589cc9c libcoreclr.so`CallDescrWorkerWithHandler(pCallDescrData=0x00007fffffffd5a8, fCriticalCall=NO) at callhelpers.cpp:70
  29. frame #28: 0x00007ffff589da1c libcoreclr.so`MethodDescCallSite::CallTargetWorker(this=0x00007fffffffd6e0, pArguments=0x00007fffffffd680, pReturnValue=0x0000000000000000, cbReturnValue=0) at callhelpers.cpp:546
  30. frame #29: 0x00007ffff56ee983 libcoreclr.so`MethodDescCallSite::Call(this=0x00007fffffffd6e0, pArguments=0x00007fffffffd680) at callhelpers.h:459
  31. frame #30: 0x00007ffff5ac1c64 libcoreclr.so`RunMainInternal(pParam=0x00007fffffffd950) at assembly.cpp:1487
  32. frame #31: 0x00007ffff5ac1989 libcoreclr.so`RunMain(this=0x00007fffffffd858, pParam=0x00007fffffffd950)::$_1::operator()(Param*) const::'lambda'(Param*)::operator()(Param*) const at assembly.cpp:1559
  33. frame #32: 0x00007ffff5abf1f9 libcoreclr.so`RunMain(this=0x00007fffffffd940, __EXparam=0x00007fffffffd950)::$_1::operator()(Param*) const at assembly.cpp:1561
  34. frame #33: 0x00007ffff5abf019 libcoreclr.so`RunMain(pFD=0x00007fff7bd5c368, numSkipArgs=1, piRetVal=0x00007fffffffda4c, stringArgs=0x00007fffffffdf20) at assembly.cpp:1561
  35. frame #34: 0x00007ffff5abf4a2 libcoreclr.so`Assembly::ExecuteMainMethod(this=0x00005555557d4d70, stringArgs=0x00007fffffffdf20, waitForOtherThreads=YES) at assembly.cpp:1671
  36. frame #35: 0x00007ffff56e8a6b libcoreclr.so`CorHost2::ExecuteAssembly(this=0x000055555578eb40, dwAppDomainId=1, pwzAssemblyPath=u"/console/bin/Release/netcoreapp3.1/console.dll", argc=0, argv=0x0000000000000000, pReturnValue=0x00007fffffffe100) at corhost.cpp:460
  37. frame #36: 0x00007ffff568822a libcoreclr.so`::coreclr_execute_assembly(hostHandle=0x000055555578eb40, domainId=1, argc=0, argv=0x0000000000000000, managedAssemblyPath="/console/bin/Release/netcoreapp3.1/console.dll", exitCode=0x00007fffffffe100) at unixinterface.cpp:407
  38. frame #37: 0x00007ffff67dfd8a libhostpolicy.so`___lldb_unnamed_symbol100$$libhostpolicy.so + 810
  39. frame #38: 0x00007ffff67e022d libhostpolicy.so`___lldb_unnamed_symbol101$$libhostpolicy.so + 45
  40. frame #39: 0x00007ffff67e095b libhostpolicy.so`corehost_main + 203
  41. frame #40: 0x00007ffff6a4b73c libhostfxr.so`___lldb_unnamed_symbol204$$libhostfxr.so + 1740
  42. frame #41: 0x00007ffff6a49ea1 libhostfxr.so`___lldb_unnamed_symbol202$$libhostfxr.so + 641
  43. frame #42: 0x00007ffff6a444f3 libhostfxr.so`hostfxr_main_startupinfo + 147
  44. frame #43: 0x00005555555623b7 dotnet`___lldb_unnamed_symbol114$$dotnet + 791
  45. frame #44: 0x0000555555562b90 dotnet`___lldb_unnamed_symbol115$$dotnet + 128
  46. frame #45: 0x00007ffff6ca3b97 libc.so.6`__libc_start_main + 231
  47. frame #46: 0x0000555555557810 dotnet`___lldb_unnamed_symbol9$$dotnet + 41

GcInfoDecoder::EnumerateLiveSlots 是枚举 Slot 的函数,GcInfoDecoder::ReportSlotToGC 是处理各个 Slot 的函数 (包括寄存器与栈),GcInfoDecoder::ReportStackSlotToGC 是处理栈上 (引用类型或 ref 类型) 本地变量的函数。

我们可以在 这个位置 下断点,然后查看解析出的各个 Slot 的信息:

  1. (lldb) b gcinfodecoder.h:679
  2. Breakpoint 8: where = libcoreclr.so`GcInfoDecoder::ReportSlotToGC(GcSlotDecoder&, unsigned int, REGDISPLAY*, bool, unsigned int, void (*)(void*, OBJECTREF*, unsigned int), void*) + 396 at gcinfodecoder.h:679, address = 0x00007ffff5a8836c
  3. (lldb) c
  4. Process 6460 resuming
  5. Process 6460 stopped
  6. * thread #1, name = 'dotnet', stop reason = breakpoint 8.1
  7. frame #0: 0x00007ffff5a8836c libcoreclr.so`GcInfoDecoder::ReportSlotToGC(this=0x00007fffffffab28, slotDecoder=0x00007fffffffa8c0, slotIndex=0, pRD=0x00007fffffffb5b0, reportScratchSlots=true, inputFlags=0, pCallBack=(libcoreclr.so`GcEnumObject(void*, OBJECTREF*, unsigned int) at gcenv.ee.common.cpp:148), hCallBack=0x00007fffffffc700)(void*, OBJECTREF*, unsigned int), void*) at gcinfodecoder.h:679
  8. 676 GcStackSlotBase spBase = pSlot->Slot.Stack.Base;
  9. 677 if( reportScratchSlots || !IsScratchStackSlot(spOffset, spBase, pRD) )
  10. 678 {
  11. -> 679 ReportStackSlotToGC(
  12. 680 spOffset,
  13. 681 spBase,
  14. 682 pSlot->Flags,
  15. (lldb) p *pSlot
  16. (const GcSlotDesc) $12 = {
  17. Slot = {
  18. RegisterNumber = 4294967216
  19. Stack = (SpOffset = -80, Base = GC_FRAMEREG_REL)
  20. }
  21. Flags = GC_SLOT_INTERIOR
  22. }

这个 Slot 代表 $rbp-80 ($rbp-0x50) 处有引用类型或 ref 类型的本地变量,在前面的内容中我们已经知道 $rbp-0x50 储存了第二个 span 对象,此外标志 GC_SLOT_INTERIOR 代表本地变量是对象中间的内存地址,而不是对象开头(对象头之后类型信息之前)的内存地址,这个标志会对 GC 标记与重定位对象产生很大的影响,微软官方称这样的变量为 Interior Pointer

继续执行 cp *pSlot 可以看到其他 Slot 的内容:

  1. # $rbp-0x40, 即第一个 span 对象
  2. (const GcSlotDesc) $13 = {
  3. Slot = {
  4. RegisterNumber = 4294967232
  5. Stack = (SpOffset = -64, Base = GC_FRAMEREG_REL)
  6. }
  7. Flags = GC_SLOT_INTERIOR
  8. }
  9. # $rbp-0x20, 即本地变量 span
  10. (const GcSlotDesc) $14 = {
  11. Slot = {
  12. RegisterNumber = 4294967264
  13. Stack = (SpOffset = -32, Base = GC_FRAMEREG_REL)
  14. }
  15. Flags = GC_SLOT_INTERIOR
  16. }
  17. # $rbp-0x30, 用于初始化数组的句柄
  18. (const GcSlotDesc) $15 = {
  19. Slot = {
  20. RegisterNumber = 4294967248
  21. Stack = (SpOffset = -48, Base = GC_FRAMEREG_REL)
  22. }
  23. Flags = GC_SLOT_BASE
  24. }
  25. # $rbp-0x28, 原始数组对象
  26. (const GcSlotDesc) $16 = {
  27. Slot = {
  28. RegisterNumber = 4294967256
  29. Stack = (SpOffset = -40, Base = GC_FRAMEREG_REL)
  30. }
  31. Flags = GC_SLOT_BASE
  32. }
  33. # $rbp-0x10, args 参数
  34. (const GcSlotDesc) $17 = {
  35. Slot = {
  36. RegisterNumber = 4294967280
  37. Stack = (SpOffset = -16, Base = GC_FRAMEREG_REL)
  38. }
  39. Flags = GC_SLOT_BASE
  40. }

标志 GC_SLOT_BASE 代表是普通的引用类型变量,指向对象的开始地址。

GC 扫描 Span 对象时的处理

接下来我们看看 GC 扫描 Span 对象时会做什么处理,尽管在上述例子中栈上保留了原始数组的地址,使用 Release 模式编译时可能会出现不保留的情况,因此 .NET Core 的运行时支持根据对象中间的地址找到对象的开始地址 (在前几年已经实现了),重新运行程序并使用以下命令可以给标记对象存活的函数下断点:

  1. (lldb) b GCHeap::Promote
  2. Breakpoint 10: 2 locations.

继续执行到达断点以后我们可以从 ppObject 得到标记对象地址的地址,这里的对象地址是第二个 span 对象中保存的开始地址,同时 flags 为 1 即 GC_CALL_INTERIOR 代表地址为对象中间的地址:

  1. (lldb) b GCHeap::Promote
  2. Breakpoint 2: 2 locations.
  3. (lldb) c
  4. Process 6636 resuming
  5. Process 6636 stopped
  6. * thread #1, name = 'dotnet', stop reason = breakpoint 2.1
  7. frame #0: 0x00007ffff5cb6dc3 libcoreclr.so`WKS::GCHeap::Promote(ppObject=0x00007fffffffd220, sc=0x00007fffffffc9b0, flags=1) at gc.cpp:36669
  8. 36666 {
  9. 36667 THREAD_NUMBER_FROM_CONTEXT;
  10. 36668 #ifndef MULTIPLE_HEAPS
  11. -> 36669 const int thread = 0;
  12. 36670 #endif //!MULTIPLE_HEAPS
  13. 36671
  14. 36672 uint8_t* o = (uint8_t*)*ppObject;
  15. (lldb) p/x *((long*)0x00007fffffffd220)
  16. (long) $0 = 0x00007fff5400ed85

因为地址在对象中间,.NET Core 运行时需要先找到对象的开始地址才能标记对象存活 (标记存活的位是类型信息的最低位),处理的代码如下 (文件):

  1. #ifdef INTERIOR_POINTERS
  2. if (flags & GC_CALL_INTERIOR)
  3. {
  4. if ((o < hp->gc_low) || (o >= hp->gc_high))
  5. {
  6. return;
  7. }
  8. if ( (o = hp->find_object (o, hp->gc_low)) == 0)
  9. {
  10. return;
  11. }
  12. }
  13. #endif //INTERIOR_POINTERS

这里会先判断地址是否在托管堆中 (如果是 stackalloc 生成的就不在),然后使用 gc_heap::find_object 来找到对象的开始地址,find_object 会先找到中间地址在 Brick 表对应的 Brick,然后找到该 Brick 对应范围中的第一个托管对象,然后一个个扫描托管对象判断地址属于哪个托管对象,如果找到属于的托管对象则使用该对象的开始地址,这是一个比较昂贵的操作。关于 Brick 表可以参考我之前写的文章

GC 重定位 Span 对象时的处理

接下来我们看看 GC 是怎么重定位 Span 对象的,先退出 LLDB 然后执行以下命令设置环境变量,这个环境变量可以强制每次 GC 的时候都启用压缩:

  1. export COMPlus_gcForceCompact=1

然后再执行 LLDB,给 GCHeap::Relocate 下断点并执行到断点:

  1. (lldb) b GCHeap::Relocate
  2. Breakpoint 2: 2 locations.
  3. (lldb) c
  4. Process 6676 resuming
  5. Process 6676 stopped
  6. * thread #1, name = 'dotnet', stop reason = breakpoint 2.2
  7. frame #0: 0x00007ffff5cb4633 libcoreclr.so`WKS::GCHeap::Relocate(ppObject=0x00007fffffffd220, sc=0x00007fffffffb810, flags=1) at gc.cpp:36741
  8. 36738 {
  9. 36739 UNREFERENCED_PARAMETER(sc);
  10. 36740
  11. -> 36741 uint8_t* object = (uint8_t*)(Object*)(*ppObject);
  12. 36742
  13. 36743 THREAD_NUMBER_FROM_CONTEXT;
  14. 36744
  15. (lldb) p/x *((long*)0x00007fffffffd220)
  16. (long) $0 = 0x00007fff5400ed85

同样的,ppObject 是标记对象地址的地址,flags 为 1 即 GC_CALL_INTERIOR。具体处理代码如下:

  1. if ((flags & GC_CALL_INTERIOR) && gc_heap::settings.loh_compaction)
  2. {
  3. if (!((object >= hp->gc_low) && (object < hp->gc_high)))
  4. {
  5. return;
  6. }
  7. if (gc_heap::loh_object_p (object))
  8. {
  9. pheader = hp->find_object (object, 0);
  10. if (pheader == 0)
  11. {
  12. return;
  13. }
  14. ptrdiff_t ref_offset = object - pheader;
  15. hp->relocate_address(&pheader THREAD_NUMBER_ARG);
  16. *ppObject = (Object*)(pheader + ref_offset);
  17. return;
  18. }
  19. }
  20. {
  21. pheader = object;
  22. hp->relocate_address(&pheader THREAD_NUMBER_ARG);
  23. *ppObject = (Object*)pheader;
  24. }

因为压缩阶段已经把对象内容移动了,重定位阶段只需要修改地址到移动后的地址,不管地址是在对象开头还是在对象中间,
对于小对象并不需要检查标记是否带有 GC_CALL_INTERIOR,直接找到对应的 Plug (relocate_address 会再次判断地址是否在托管堆中),
获取 Plug 中保存的偏移值,然后让地址减去该偏移值即可。而大对象则需要使用 find_object 来先定位对象的开始地址,以提升处理效率。

至此我们可以发现,因为 .NET 可以只根据 Span 找到原始对象并实现标记与重定位,所以 Span 原理上是可以保存在堆上的,但这需要牺牲一定性能支持线程安全与放弃 stackalloc (或者分离到另一个类型),所以微软没有选择这么做。

参考链接

写在最后

在这里打个小广告,我与柠檬编写的书籍《.NET Core 底层入门》在一月份出版了,出版社是北京航空航天大学出版社,你可以查看以下网站,找到内容介绍与购买链接:

https://netcoreimpl.github.io

或者直接访问京东的购买链接

https://item.jd.com/12796746.html

最后传播一下正能量,最近这段时间大家都不容易,我目前也没有收入来源,但我们仍然需要摆正心态,相信祖国,支持政府一同抗击疫情。
中国加油!武汉加油! 国有战,召必回,战必胜!

编译调试 .NET Core 5.0 Preview 并分析 Span 的实现原理的更多相关文章

  1. 用VS Code体验调试.NET Core 2.0 Preview (传统三层架构)

    准备工作 VS Code下载地址:https://vscode.cdn.azure.cn/stable/379d2efb5539b09112c793d3d9a413017d736f89/VSCodeS ...

  2. 【译】.NET Core 3.0 Preview 3中关于ASP.NET Core的更新内容

      .NET Core 3.0 Preview 3已经推出,它包含了一系列关于ASP.NET Core的新的更新. 下面是该预览版的更新列表: Razor组件改进: 单项目模板 新的Razer扩展 E ...

  3. .NET Core 3.0 Preview 6中对ASP.NET Core和Blazor的更新

    我们都知道在6月12日的时候微软发布了.NET Core 3.0的第6个预览版.针对.NET Core 3.0的发布我们国内的微软MVP-汪宇杰还发布的官翻版的博文进行了详细的介绍.具体的可以关注&q ...

  4. 使用VS Code从零开始开发调试.NET Core 1.0

    使用VS Code 从零开始开发调试.NET Core 1.0. .NET Core 是一个开源的.跨平台的 .NET 实现. VS Code 全称是 Visual Studio Code,Visua ...

  5. 使用VS Code开发调试.NET Core 2.0

    使用VS Code 从零开始开发调试.NET Core 2.0.无需安装VS 2017 15.3+即可开发调试.NET Core 2.0应用. VS Code 全称是 Visual Studio Co ...

  6. vscode 调试.net core 2.0 输出乱码解决方法

    之前在vscode上调试.net core 2.0项目时输出窗口一直是乱码,查了很多资料无法解决 最终在github找到了解决办法 ->   https://github.com/OmniSha ...

  7. .NetCore技术研究-EntityFramework Core 3.0 Preview

    前段时间.Net Core 3.0 发布了,Entity Framework Core 3.0 也发布了Preview版.假期用了一上午大致研究了一遍,同时又体验了一把Visual Studio 20 ...

  8. .NET Core 2.0 Preview 1发布下载和文档

    .NET Core 2.0.0 Preview 1 发布于 2017 5.10. 你可以通过 Visual Studio 2017 Preview 15.3, Visual Studio for Ma ...

  9. asp.net core 1.1 项目升级至 asp.net core 2.0 preview 2 与正式版

    这两天把一个 asp.net core 1.1 的项目迁移到了 asp.net core 2.0 preview 2 ,在这篇随笔中记录一下. 如果项目在有 global.json 文件,需要删除或修 ...

随机推荐

  1. iptables匹配端口范围,映射,网络状态

    ####匹配端口范围:iptables -I INPUT -p tcp -m multiport --dport 21,22,23,24 -j ACCEPT <==次选iptables -I I ...

  2. $POJ1015\ Jury\ Compromise\ Dp$/背包

    洛谷传送门 $Sol$ 这是一道具有多个“体积维度”的$0/1$背包问题. 把$N$个候选人看做$N$个物品,那么每个物品有如下三种体积: 1.“人数”,每个候选人的“人数”都是$1$,最终要填满容积 ...

  3. SpringBoot拦截器没有生效的原因

    最近的一个项目中,需要实现一个记录系统请求日志的功能,我通过拦截器实现了这个功能,但是,测试的时候发现拦截器没有发挥作用,检查了很久,没有发现错的地方,系统也没有报错,最终在网上找打了答案. 原来之前 ...

  4. hadoop参数传递实例

    要求: 根据输入文件中的信息,计算出某几个字符串出现的个数 输入文件格式:xxx,xxx,xxx,xx,x,x,xxx,x,x,xx,x,x,x,x,x,x,x, 输出文件:xx    10 xx   ...

  5. 快速开发一个npm包(轮子)

    动机 很多人都想写一个自己的轮子,可是开始动手的时候你总会遇到以下问题 一个基本的 js 库应该如何编写 基本的前端项目都要哪些文件 又要怎么打包发布到 npm 上 你的 es6 语法如何才能让别人识 ...

  6. Collections 工具类

    针对 List 集合的方法 排序 sort 如果集合元素为基本数据类型,采用快排:对于集合元素为引用类型,采用归并排序. //对指定 List 集合的元素按照自然排序 void sort(List&l ...

  7. .Net 面试题整理(一)

    1.C# 的三大特性? 封装.继承.多态 2.简述 private. protected. public. internal 修饰符的访问权限. private : 私有成员, 在类的内部才可以访问. ...

  8. Go Web 编程之 Hello World

    概述 计划写一个讲 Go Web 编程的系列文章.从基于 net/http 包编写 Go Web 程序开始,讲述处理器,请求,响应等基础知识.然后到框架的使用.中间会穿插一些源码的分析.最后做一个实战 ...

  9. php改变时间的方法

    1. strtotime date("Y-m-d",strtotime('+1day')) 2. mktime date("Y-m-d",mktime(0,0, ...

  10. 案例分析丨H&M用设计冲刺将App研发周期缩短为6个月

    案例背景 H&M是一家来自瑞典的时装公司,1947年成立.截至2018年6月,H&M 分店遍布全球 68 个国家和地区,分店数目为 4338 间. 作为快速服装生产商,H&M的 ...