范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文

译。NET7中的性能改进(五)

  回 复  " 1 "  获 取  开 发 者 路 线 图
  学 习 分 享    丨 作 者   /   郑   子   铭
  这 是  D o t N e t   N B   公 众 号  的 第 210   篇 原 创 文 章
  原文 | Stephen Toub
  翻译 | 郑子铭循环提升和克隆 (Loop Hoisting and Cloning)
  我们之前看到PGO是如何与循环提升和克隆互动的,这些优化也有其他改进。
  从历史上看,JIT对提升的支持仅限于将一个不变量提升到一个层级。
  考虑一下这个例子:[Benchmark]
  public void Compute()
  {
  for (int thousands = 0; thousands < 10; thousands++)
  {
  for (int hundreds = 0; hundreds < 10; hundreds++)
  {
  for (int tens = 0; tens < 10; tens++)
  {
  for (int ones = 0; ones < 10; ones++)
  {
  int n = ComputeNumber(thousands, hundreds, tens, ones);
  Process(n);
  }
  }
  }
  }
  }
  static int ComputeNumber(int thousands, int hundreds, int tens, int ones) =>
  (thousands * 1000) +
  (hundreds * 100) +
  (tens * 10) +
  ones;
  [MethodImpl(MethodImplOptions.NoInlining)]
  static void Process(int n) { }
  乍一看,你可能会说:"有什么可提升的,n的计算需要所有的循环输入,而所有的计算都在ComputeNumber中。" 但从编译器的角度来看,ComputeNumber函数是可内联的,因此在逻辑上可以成为其调用者的一部分,n的计算实际上被分成了多块,每块都可以被提升到不同的层级,例如,十的计算可以提升出一层,百的提升出两层,千的提升出三层。下面是[DisassemblyDiagnoser]对.NET 6的输出。; Program.Compute()
  push r14
  push rdi
  push rsi
  push rbp
  push rbx
  sub rsp,20
  xor esi,esi
  M00_L00:
  xor edi,edi
  M00_L01:
  xor ebx,ebx
  M00_L02:
  xor ebp,ebp
  imul ecx,esi,3E8
  imul eax,edi,64
  add ecx,eax
  lea eax,[rbx+rbx*4]
  lea r14d,[rcx+rax*2]
  M00_L03:
  lea ecx,[r14+rbp]
  call Program.Process(Int32)
  inc ebp
  cmp ebp,0A
  jl short M00_L03
  inc ebx
  cmp ebx,0A
  jl short M00_L02
  inc edi
  cmp edi,0A
  jl short M00_L01
  inc esi
  cmp esi,0A
  jl short M00_L00
  add rsp,20
  pop rbx
  pop rbp
  pop rsi
  pop rdi
  pop r14
  ret
  ; Total bytes of code 84
  我们可以看到,这里发生了一些提升。毕竟,最里面的循环(标记为M00_L03)只有五条指令:增加ebp(这时是1的计数器值),如果它仍然小于0xA(10),就跳回到M00_L03,把r14中的任何数字加到1上。很好,所以我们已经把所有不必要的计算从内循环中取出来了,只剩下把1的位置加到其余的数字中。让我们再往外走一级。M00_L02是十位数循环的标签。我们在这里看到了什么?有问题。两条指令imul ecx,esi,3E8和imul eax,edi,64正在进行千位数1000和百位数100的操作,突出表明这些本来可以进一步提升的操作被卡在了最下层的循环中。现在,这是我们在.NET 7中得到的结果,在dotnet/runtime#68061中,这种情况得到了改善:; Program.Compute()
  push r15
  push r14
  push r12
  push rdi
  push rsi
  push rbp
  push rbx
  sub rsp,20
  xor esi,esi
  M00_L00:
  xor edi,edi
  imul ebx,esi,3E8
  M00_L01:
  xor ebp,ebp
  imul r14d,edi,64
  add r14d,ebx
  M00_L02:
  xor r15d,r15d
  lea ecx,[rbp+rbp*4]
  lea r12d,[r14+rcx*2]
  M00_L03:
  lea ecx,[r12+r15]
  call qword ptr [Program.Process(Int32)]
  inc r15d
  cmp r15d,0A
  jl short M00_L03
  inc ebp
  cmp ebp,0A
  jl short M00_L02
  inc edi
  cmp edi,0A
  jl short M00_L01
  inc esi
  cmp esi,0A
  jl short M00_L00
  add rsp,20
  pop rbx
  pop rbp
  pop rsi
  pop rdi
  pop r12
  pop r14
  pop r15
  ret
  ; Total bytes of code 99
  现在注意一下这些imul指令的位置。有四个标签,每个标签对应一个循环,我们可以看到最外层的循环有imul ebx,esi,3E8(用于千位计算),下一个循环有imul r14d,edi,64(用于百位计算),突出表明这些计算被提升到了适当的层级(十位和一位计算仍然在正确的位置)。
  在克隆方面有了更多的改进。以前,循环克隆只适用于从低值到高值的1次迭代循环。有了dotnet/runtime#60148,与上值的比较可以是<=,而不仅仅是<。有了dotnet/runtime#67930,向下迭代的循环也可以被克隆,增量和减量大于1的循环也是如此。private int[] _values = Enumerable.Range(0, 1000).ToArray();
  [Benchmark]
  [Arguments(0, 0, 1000)]
  public int LastIndexOf(int arg, int offset, int count)
  {
  int[] values = _values;
  for (int i = offset + count - 1; i >= offset; i--)
  if (values[i] == arg)
  return i;
  return 0;
  }
  如果没有循环克隆,JIT不能假设offset到offset+count都在范围内,因此对数组的每个访问都需要进行边界检查。有了循环克隆,JIT可以生成一个没有边界检查的循环版本,并且只在它知道所有的访问都是有效的时候使用。这正是现在.NET 7中发生的事情。下面是我们在.NET 6中得到的情况。; Program.LastIndexOf(Int32, Int32, Int32)
  sub rsp,28
  mov rcx,[rcx+8]
  lea eax,[r8+r9+0FFFF]
  cmp eax,r8d
  jl short M00_L01
  mov r9d,[rcx+8]
  nop word ptr [rax+rax]
  M00_L00:
  cmp eax,r9d
  jae short M00_L03
  movsxd r10,eax
  cmp [rcx+r10*4+10],edx
  je short M00_L02
  dec eax
  cmp eax,r8d
  jge short M00_L00
  M00_L01:
  xor eax,eax
  add rsp,28
  ret
  M00_L02:
  add rsp,28
  ret
  M00_L03:
  call CORINFO_HELP_RNGCHKFAIL
  int 3
  ; Total bytes of code 72
  注意在核心循环中,在标签M00_L00处,有一个边界检查(cmp eax,r9d and jae short M00_L03,它跳到一个调用CORINFO_HELP_RNGCHKFAIL)。而这里是我们在.NET 7中得到的结果。; Program.LastIndexOf(Int32, Int32, Int32)
  sub rsp,28
  mov rax,[rcx+8]
  lea ecx,[r8+r9+0FFFF]
  cmp ecx,r8d
  jl short M00_L02
  test rax,rax
  je short M00_L01
  test ecx,ecx
  jl short M00_L01
  test r8d,r8d
  jl short M00_L01
  cmp [rax+8],ecx
  jle short M00_L01
  M00_L00:
  mov r9d,ecx
  cmp [rax+r9*4+10],edx
  je short M00_L03
  dec ecx
  cmp ecx,r8d
  jge short M00_L00
  jmp short M00_L02
  M00_L01:
  cmp ecx,[rax+8]
  jae short M00_L04
  mov r9d,ecx
  cmp [rax+r9*4+10],edx
  je short M00_L03
  dec ecx
  cmp ecx,r8d
  jge short M00_L01
  M00_L02:
  xor eax,eax
  add rsp,28
  ret
  M00_L03:
  mov eax,ecx
  add rsp,28
  ret
  M00_L04:
  call CORINFO_HELP_RNGCHKFAIL
  int 3
  ; Total bytes of code 98
  注意到代码大小是如何变大的,以及现在有两个循环的变化:一个在 M00_L00,一个在 M00_L01。第二个,M00_L01,有一个分支到那个相同的调用 CORINFO_HELP_RNGCHKFAIL,但第一个没有,因为那个循环最终只会在证明偏移量、计数和 _values.Length 是这样的,即索引将总是在界内之后被使用。
  dotnet/runtime#59886使JIT能够选择不同的形式来发出选择快速或慢速循环路径的条件,例如,是否发出所有的条件,与它们一起,然后分支(if (! (cond1 & cond2)) goto slowPath),或者是否单独发出每个条件(if (!cond1) goto slowPath; if (!cond2) goto slowPath)。 dotnet/runtime#66257使循环变量被初始化为更多种类的表达式时,循环克隆得以启动(例如,for (int fromindex = lastIndex - lengthToClear; ...) )。dotnet/runtime#70232增加了JIT克隆具有更广泛操作的主体的循环的意愿。折叠、传播和替换 (Folding, propagation, and substitution)
  常量折叠是一种优化,编译器在编译时计算只涉及常量的表达式的值,而不是在运行时生成代码来计算该值。在.NET中有多个级别的常量折叠,有些常量折叠由C#编译器执行,有些常量折叠由JIT编译器执行。例如,给定C#代码。[Benchmark]
  public int A() => 3 + (4 * 5);
  [Benchmark]
  public int B() => A() * 2;
  C#编译器将为这些方法生成IL,如下所示。.method public hidebysig instance int32 A () cil managed
  {
   .maxstack 8
   IL_0000: ldc.i4.s 23
   IL_0002: ret
  }
  
  .method public hidebysig instance int32 B () cil managed
  {
   .maxstack 8
   IL_0000: ldarg.0
   IL_0001: call instance int32 Program::A()
   IL_0006: ldc.i4.2
   IL_0007: mul
   IL_0008: ret
  }
  你可以看到,C#编译器已经计算出了3+(4*5)的值,因为方法A的IL只是包含了相当于返回23;的内容。然而,方法B包含了相当于return A() * 2;的内容,突出表明C#编译器所进行的常量折叠只是在方法内部进行的。现在是JIT生成的内容。; Program.A()
  mov eax,17
  ret
  ; Total bytes of code 6
  ; Program.B()
  mov eax,2E
  ret
  ; Total bytes of code 6
  方法A的汇编并不特别有趣;它只是返回相同的值23(十六进制0x17)。但方法B更有趣。JIT已经内联了从B到A的调用,将A的内容暴露给B,这样JIT就有效地将B的主体视为等同于返回23*2;。在这一点上,JIT可以做自己的常量折叠,它将B的主体转化为简单的返回46(十六进制0x2e)。常量传播与常量折叠有着错综复杂的联系,本质上就是你可以将一个常量值(通常是通过常量折叠计算出来的)替换到进一步的表达式中,这时它们也可以被折叠。
  JIT长期以来一直在进行恒定折叠,但它在.NET 7中得到了进一步改善。常量折叠的改进方式之一是暴露出更多需要折叠的值,这往往意味着更多的内联。dotnet/runtime#55745帮助inliner理解像M(constant + constant)这样的方法调用(注意到这些常量可能是其他方法调用的结果)本身就是在向M传递常量,而常量被传递到方法调用中是在提示inliner应该考虑更积极地进行内联,因为将该常量暴露给被调用者的主体有可能大大减少实现被调用者所需的代码量。JIT之前可能已经内联了这样的方法,但是当涉及到内联时,JIT是关于启发式方法和产生足够的证据来证明值得内联的东西;这有助于这些证据。例如,这种模式出现在TimeSpan的各种FromXx方法中。例如,TimeSpan.FromSeconds被实现为。public static TimeSpan FromSeconds(double value) => Interval(value, TicksPerSecond); // TicksPerSecond is a constant
  并且,为了这个例子的目的,避开了参数验证,Interval是。private static TimeSpan Interval(double value, double scale) => IntervalFromDoubleTicks(value * scale);
  private static TimeSpan IntervalFromDoubleTicks(double ticks) => ticks == long.MaxValue ? TimeSpan.MaxValue : new TimeSpan((long)ticks);
  如果所有的东西都被内联,意味着FromSeconds本质上是。public static TimeSpan FromSeconds(double value)
  {
  double ticks = value * 10_000_000;
  return ticks == long.MaxValue ? TimeSpan.MaxValue : new TimeSpan((long)ticks);
  }
  如果值是一个常数,比方说5,整个事情可以被常数折叠(在ticks == long.MaxValue分支上消除了死代码),简单地说。return new TimeSpan(50_000_000);
  我就不说.NET 6的程序集了,但在.NET 7上,用这样的基准来衡量。[Benchmark]
  public TimeSpan FromSeconds() => TimeSpan.FromSeconds(5);
  我们现在得到的是简单和干净。; Program.FromSeconds()
  mov eax,2FAF080
  ret
  ; Total bytes of code 6
  另一个改进常量折叠的变化包括来自@SingleAccretion的dotnet/runtime#57726,它在一种特殊的情况下解除了常量折叠,这种情况有时表现为对从方法调用返回的结构进行逐字段赋值。作为一个小例子,考虑这个微不足道的属性,它访问了Color.DarkOrange属性,而后者又做了new Color(KnownColor.DarkOrange)。[Benchmark]
  public Color DarkOrange() => Color.DarkOrange;
  在.NET 6中,JIT生成了这个。; Program.DarkOrange()
  mov eax,1
  mov ecx,39
  xor r8d,r8d
  mov [rdx],r8
  mov [rdx+8],r8
  mov [rdx+10],cx
  mov [rdx+12],ax
  mov rax,rdx
  ret
  ; Total bytes of code 32
  有趣的是,一些常量(39,是KnownColor.DarkOrange的值,和1,是一个私有的StateKnownColorValid常量)被加载到寄存器中(mov eax, 1 then mov ecx, 39),然后又被存储到被返回的颜色结构的相关位置(mov [rdx+12],ax and mov [rdx+10],cx)。在.NET 7中,它现在产生了。; Program.DarkOrange()
  xor eax,eax
  mov [rdx],rax
  mov [rdx+8],rax
  mov word ptr [rdx+10],39
  mov word ptr [rdx+12],1
  mov rax,rdx
  ret
  ; Total bytes of code 25
  直接将这些常量值分配到它们的目标位置(mov word ptr [rdx+12],1 和 mov word ptr [rdx+10],39)。其他有助于常量折叠的变化包括来自@SingleAccretion的dotnet/runtime#58171和来自@SingleAccretion的dotnet/runtime#57605。
  然而,一大类改进来自与传播有关的优化,即正向替换。考虑一下这个愚蠢的基准。[Benchmark]
  public int Compute1() => Value + Value + Value + Value + Value;
  [Benchmark]
  public int Compute2() => SomethingElse() + Value + Value + Value + Value + Value;
  private static int Value => 16;
  [MethodImpl(MethodImplOptions.NoInlining)]
  private static int SomethingElse() => 42;
  如果我们看一下在.NET 6上为Compute1生成的汇编代码,它看起来和我们希望的一样。我们将Value加了5次,Value被简单地内联,并返回一个常量值16,因此我们希望为Compute1生成的汇编代码实际上只是返回值80(十六进制0x50),这正是发生的情况。; Program.Compute1()
  mov eax,50
  ret
  ; Total bytes of code 6
  但Compute2有点不同。代码的结构是这样的:对SomethingElse的额外调用最终会稍微扰乱JIT的分析,而.NET 6最终会得到这样的汇编代码。; Program.Compute2()
  sub rsp,28
  call Program.SomethingElse()
  add eax,10
  add eax,10
  add eax,10
  add eax,10
  add eax,10
  add rsp,28
  ret
  ; Total bytes of code 29
  我们不是用一个mov eax, 50来把数值0x50放到返回寄存器中,而是用5个独立的add eax, 10来建立同样的0x50(80)的数值。这......并不理想。
  事实证明,许多JIT的优化都是在解析IL的过程中创建的树状数据结构上进行的。在某些情况下,当它们接触到更多的程序时,优化可以做得更好,换句话说,当它们所操作的树更大,包含更多需要分析的内容时。然而,各种操作可以将这些树分解成更小的、单独的树,比如作为内联的一部分而创建的临时变量,这样做可以抑制这些操作。为了有效地将这些树缝合在一起,我们需要一些东西,这就是前置置换 (forward substitution)。你可以把前置置换看成是CSE的逆向操作;与其通过计算一次数值并将其存储到一个临时变量中来寻找重复的表达式并消除它们,不如前置置换来消除这个临时变量并有效地将表达式树移到它的使用位置。显然,如果这样做会否定CSE并导致重复工作的话,你是不想这样做的,但是对于那些只定义一次并使用一次的表达式来说,这种前置传播是很有价值的。 dotnet/runtime#61023添加了一个最初的有限的前置置换版本,然后dotnet/runtime#63720添加了一个更强大的通用实现。随后,dotnet/runtime#70587将其扩展到了一些SIMD向量,然后dotnet/runtime#71161进一步改进了它,使其能够替换到更多的地方(在这种情况下是替换到调用参数)。有了这些,我们愚蠢的基准现在在.NET 7上产生如下结果。; Program.Compute2()
  sub rsp,28
  call qword ptr [7FFCB8DAF9A8]
  add eax,50
  add rsp,28
  ret
  ; Total bytes of code 18
  原文链接
  Performance Improvements in .NET 7
  推荐阅读: 【译】.NET 7 中的性能改进(四)
  【译】.NET 7 中的性能改进(三)
  【译】.NET 7 中的性能改进(二)
  【译】.NET 7 中的性能改进(一)
  一款针对EF Core轻量级分表分库、读写分离的开源项目
  跟我一起 掌握AspNetCore底层技术和构建原理
  点击下方卡片关注DotNet NB
  一起交流学习
  点击上方卡片关注DotNet NB,一起交流学习
  请在公众号后台 回复  【路线图】  获取.NET 2021开发者路线图  回复  【原创内容】 获取公众号原创内容 回复  【峰会视频】 获取.NET Conf开发者大会视频 回复  【个人简介】 获取作者个人简介 回复  【年终总结】 获取作者年终总结 回复   【 加群 】  加入DotNet NB 交流学习群
  和我一起,交流学习,分享心得。

梧桐山老虎涧大瀑布梧桐山里藏着许多瀑布,比较的的瀑布有三个,一个是泰山涧里面,瀑布高约10米,瀑布在山腰,水量要小一点。第二个就是梧桐山北大门旁边进去一点点,有一个,因为在山脚,水量比较大,夏天去那大美中国端午时节纵览祖国各地美景央视网消息林海连绵,莽莽苍苍,画面中是河北塞罕坝机械林场。嫩绿的落叶松随风摇曳,湛蓝的七星湖碧波荡漾,静寂的望海楼镇守山巅。眼下,林场正抢抓时机,在干旱沙地石质荒山上种树植绿。数十出去旅游的意义一般来说大家认为出去旅游就是去一个没去过的地方,看个稀奇,享受一下自己的假日。当然,也有说是去体验风土人情的。还有认为旅游是去放空心情的,因为长期的工作与日复一日地在一个城市,去另大美的中国大美的时代原标题大美的中国大美的时代每年4月,安徽省砀山县黄河故道两岸万顷梨园的梨花竞相绽放,吸引了众多游客前来游览。崔猛摄在黑龙江省密山市,特色农业万寿菊丰收,田间地头随处都能看到村民采收宫崎骏动漫原型地原来在这天空之城意大利白露里治奥古老的小村庄位于山谷中一座巨大的石灰岩上,像天空之城里面的场景,灵动梦幻。从远处看,云雾缭绕的城堡若隐若现,犹如漂浮在空中,它就是天空之城的代名词。千与千寻在沙特出境时注意这一点,否则会被禁止入境三年护照总局表示如果外籍用出境和再入境签证(ExitandReentryvisa)离开沙特,并且没有在特定的日期内再次返回沙特,该外籍将被禁止入境沙特三年。TheGeneralDire悉尼音乐节终于回归了,可中国游客何在?当地时间5月27日,2022年活力悉尼灯光音乐节在悉尼开幕。(图央视网)澳洲网评论员陈奥近日的悉尼处处流光溢彩。停办了两年的缤纷悉尼灯光音乐节于5月27日至6月18日重磅回归,整个新疆美食美景版爱你来啦如果你突然打了个喷嚏那一定就是我在想你如果半夜被手机吵醒啊那是因为我关心近日王心凌凭借重唱一首自己的经典歌曲爱你再次翻红一场穿越青春的甜蜜旋风让大家得空就想爱你一下新疆当然也不例外中国女排一战收获三大惊喜!龚翔宇打出巨星水准,王云蕗立功了世联赛首战,中国女排在01落后的不利局面下,连扳3局完成逆转,蔡斌指导收获开门红。本场比赛,中国女排收获三大惊喜,其中接应位置上的龚翔宇,在攻防两端都发挥出了世界顶级巨星级的水准,万众瞩目的揭幕战,升班马浙江队会不会给山东泰山上一课?随着中超开赛越来越近了,重庆两江竞技解散,大连人递补,对于这个结果,大家都没有话可以说,毕竟足协追求完整性,虽然大连人有些不情愿进入中超,但是他们权衡之后,还是决定进入中超,毕竟经试训失败!山东锋线悍将遭NBL球队抛弃,恐无球可打,或就此退役北京时间6月1日消息,六一儿童节,老鬼侃篮球祝各位读者朋友,节日快乐,天天快乐!山东高速男篮正在济南进行集训备战,而山东男篮锋线悍将王冠翔去NBL球队试训,却以失败告终了。这位朱荣
过度保护真会害了孩子!12岁还要奶奶喂饭男孩的经历,点醒了很多人每个家长都希望自己的孩子能够自信快乐地长大于是对孩子百般爱护殊不知这些行为可能会害了孩子网络配图,与事件无关13岁的男孩刚刚(化名),因为情绪总是不好,于是去了河北省第六人民医院就陇上幸福生活烟火静宁陇上幸福生活烟火静宁文郭三省乡下人要到县里去,无论东南西北,统统叫上县省市的人到县里去,不管上下左右,一律叫下县。于是,县城,也就成了不上不下亦上亦下的地儿。静宁地处312国道沿线安全守护落到食处天穆镇恒逸华庭社区开展幼儿园食品安全检查随着开学日的到来,为进一步加强社区幼儿园安全监管工作,避免和减少各类食品安全事故的发生。2月13日,恒逸华庭社区居委会对幼儿园开展了食品安全专项检查。居委会工作人员到恒逸华庭幼儿园与其费尽心思鼓励三胎四胎,不如先让不孕不育进入医保在过去的几年里,新生儿的出生人数牵动着无数人的心,更是屡屡冲上社会的热门话题。尤其是二胎政策开放以后,很多人都期待出生人数触底反弹,为日益老龄化的人口注入新鲜活力。如今已经是202关注江津区召开红色三线文创文旅交流座谈会根据中共江津区委关于做好江津区红色三线文化和精神传承弘扬工作,做好江津区红色三线文创文旅的创新和发展工作的要求。探讨江津区红色三线文化旅游融合发展助推红色三线文创文旅研学基地创建与跟随老韩走遍中国,宁夏篇之一银川市(下)本文为走遍中国的长篇游记,每日更新,希望大家喜欢关注点赞并转发,谢谢!网友留言天下黄河富宁夏,可见宁夏是个好地方!宁夏号称塞上江南,地处河套平原的银川周围和江南地区农村差不多,很富福建宁德城隍巡游祈平安城隍巡游现场,人山人海。周思颖供图中新网宁德2月16日电(周思颖)锣鼓喧天,鞭炮齐鸣。2月15日(农历正月二十五日)晚,阔别3年的闹春会重头戏城隍巡游在福建省宁德市蕉城区举行。城隍冬徙三亚,夏居黑钻贵阳铁建城美出圈的度假式园林生活近期,不断有三亚20万一晚的房间被预定完22小时只开3公里,三亚成最堵城市等词条不断冲上热搜。2023年春节暖消费报告显示春节期间,海南春节期间度假商品订单量同比增长24民宿订单量组图晶莹剔透娇艳欲滴!琼岛早春桃花朵朵迷人眼晶莹剔透的粉色花朵非常漂亮。记者陈卫东摄新海南客户端南海网南国都市报2月15日消息(记者陈卫东)琼岛春来早,桃花朵朵开。2月14日,记者在儋州两院华南热带作物科学研究院与华南热带作给满江红主演演技排名沈腾雷佳音难分伯仲,易烊千玺第四张艺谋新片满江红超越了流浪地球2,目前已是春节档票房冠军,这也是他票房表现最好的电影,可喜可贺。满江红的主演有6位,演技差距还是挺明显的,八先生姑且冒昧地给他们的演技排个名。(下文70岁的老人,还建议带去旅游吗?70岁的老人,还建议带去旅游吗记得疫情刚好转的一年,抽空带父母出了一趟远门。先交代一下,父亲那年刚好70岁过了,母亲是60岁。出门前给父母买了7天的意外险,就是怕在路上磕着绊着,所