汇编 – Haswell/Skylake的部分寄存器究竟如何执行?写AL似乎对RAX有假依赖,而AH是不一致的

前端之家收集整理的这篇文章主要介绍了汇编 – Haswell/Skylake的部分寄存器究竟如何执行?写AL似乎对RAX有假依赖,而AH是不一致的前端之家小编觉得挺不错的,现在分享给大家,也给大家做个参考。
这个循环在Intel Conroe / Merom上每3个循环运行一次,如预期的那样在imul吞吐量上存在瓶颈。但是在Haswell / Skylake上,它每11个循环运行一次,显然是因为setnz al依赖于最后一个imul。
; synthetic micro-benchmark to test partial-register renaming
    mov     ecx,1000000000
.loop:                 ; do{
    imul    eax,eax     ; a dep chain with high latency but also high throughput
    imul    eax,eax
    imul    eax,eax

    dec     ecx          ; set ZF,independent of old ZF.  (Use sub ecx,1 on Silvermont/KNL or P4)
    setnz   al           ; ****** Does this depend on RAX as well as ZF?
    movzx   eax,al
    jnz  .loop         ; }while(ecx);@H_301_2@ 
 

如果setnz依赖于rax,则3ximul / setcc / movzx序列形成循环携带的依赖链。如果没有,每个setcc / movzx / 3ximul链是独立的,从更新循环计数器的dec分叉。在HSW / SKL上测量的每次迭代11c完全由延迟瓶颈解释:3x3c(imul)1c(由setcc读取 – 修改 – 写入)1c(movzx在同一寄存器中)。

偏离主题:避免这些(故意)瓶颈

我采用可理解/可预测的行为来隔离部分注册的东西,而不是最佳性能

例如,xor-zero / set-flags / setcc无论如何都更好(在这种情况下,xor eax,eax / dec ecx / setnz al)。这打破了所有cpu上的eax(除了像PII和PIII这样的早期P6系列),仍然避免了部分寄存器合并处罚,并节省了1c的movzx延迟。它还在handle xor-zeroing in the register-rename stagecpu上使用少一个ALU uop。有关使用setcc的xor-zeroing的更多信息,请参阅该链接

请注意,AMD,Intel Silvermont / KNL和P4根本不进行部分寄存器重命名。它只是英特尔P6系列cpu及其后代英特尔Sandybridge系列中的一项功能,但似乎已逐步淘汰。

不幸的是gcc倾向于使用cmp / setcc al / movzx eax,al它可以使用xor而不是movzx (Godbolt compiler-explorer example),而clang使用xor-zero / cmp / setcc,除非你结合多个布尔条件,如count =(a == b) )| (A ==〜B)。

xor / dec / setnz版本在Skylake,Haswell和Core2上每次迭代运行3.0c(在imul吞吐量方面存在瓶颈)。 xor-zeroing打破了除了PPro / PII / PIII /早期Pentium-M之外的所有无序cpu对eax旧值的依赖性(它仍然避免了部分寄存器合并处罚,但没有打破dep )。 Agner Fog’s microarch guide describes this.使用mov eax替换xor-zeroing时,0将它降低到Core2:2-3c stall (in the front-end?) to insert a partial-reg merging uop时每4.78个周期减1,当imul在setnz al之后读取eax时。

另外,我使用了movzx eax,它击败了mov-elimination,就像mov rax一样,rax也是如此。 (IvB,HSW和SKL可以重命名movzx eax,bl具有0延迟,但Core2不能)。除了部分寄存器行为之外,这使得Core2 / SKL上的所有内容都相同。

Core2行为与Agner Fog’s microarch guide一致,但HSW / SKL行为不符合。从第11.10节到Skylake,以及之前的英特尔搜索

Different parts of a general purpose register can be stored in different temporary registers in order to remove false dependences.

遗憾的是,他没有时间对每个新的uarch进行详细的测试,以重新测试假设,因此这种行为的变化滑过了裂缝。

Agner确实描述了通过Skylake在Sandybridge上插入high8寄存器(AH / BH / CH / DH)以及SnB上的low8 / low16插入(不停止)合并uop。 (遗憾的是,我过去常常传播错误的信息,并说Haswell可以免费合并AH。我过快地浏览了Agner的Haswell部分,并没有注意到后面关于high8寄存器的段落。如果你看到了,请告诉我。我对其他帖子的错误评论,所以我可以删除它们或添加更正。我会尝试至少找到并编辑我已经说过的答案。)

我的实际问题:部分寄存器在Skylake上的表现究竟如何?

从IvyBridge到Skylake的一切都是一样的,包括高8的额外延迟?

Intel’s optimization manual没有具体说明哪些cpu具有错误依赖性(虽然它确实提到某些cpu具有它们),并且省略了诸如读取AH / BH / CH / DH(high8寄存器)之类的东西,即使它们没有被修改了。

如果Agner Fog的微观指南没有描述任何P6系列(Core2 / Nehalem)行为,那也会很有趣,但我应该将这个问题的范围限制在Skylake或Sandybridge家族。

我的Skylake测试数据,将%rep 4短序列放入一个运行100M或1G迭代的小型dec ebp / jnz循环中。我使用相同的硬件(桌面Skylake i7 6700k)测试Linux的周期与in my answer here相同。

除非另有说明,否则每条指令都使用ALU执行端口作为1个融合域uop运行。 (用ocperf.py stat -e ...,uops_issued.any,uops_executed.thread测量)。这检测到(没有)mov-elimination和额外的合并uops。

“每循环4个”案例是对无限展开案例的推断。循环开销占用了一些前端带宽,但是每个周期优于1的任何东西都表明寄存器重命名避免了write-after-write output dependency,并且uop在内部不作为读 – 修改 – 写处理。

仅写入AH:阻止循环从环回缓冲区(也称为循环流检测器(LSD))执行。 lsd.uops的计数在HSW上正好为0,在SKL上为小(约为1.8k)并且不随循环迭代计数而缩放。可能这些计数来自某些内核代码。当循环从LSD运行时,lsd.uops~ = uops_issued到测量噪声内。一些循环在LSD或no-LSD之间交替(例如,如果解码在错误的位置开始,它们可能不适合uop缓存),但是在测试时我没有碰到它。

>重复mov ah,bh和/或mov ah,bl每周期运行4次。它需要一个ALU uop,所以它不像mov eax那样被消除,ebx是。
>重复mov啊,[rsi]每周期运行2次(负载吞吐量瓶颈)。
>重复mov啊,123每循环运行1次。 (循环内部的dep-breaking xor eax,eax消除了瓶颈。)
>重复setz啊或setc啊每循环运行1次。 (一个破坏性的xor eax,eax让它成为setcc和循环分支的p06吞吐量的瓶颈。)

为什么用通常使用ALU执行单元的指令写入啊对旧值有错误的依赖,而mov r8,r / m8没有(对于reg或内存src)? (那么对于mov r / m8,r8呢?肯定你用于reg-reg的两个操作码中的哪一个移动无关紧要?)
>重复添加啊,每个周期运行123次,如预期的那样。
>重复添加dh,cl每循环运行1次。
>重复添加dh,dh每循环运行1次。
>重复添加dh,每循环运行0.5次。读取[ABCD] H在它们“干净”时是特殊的(在这种情况下,RCX最近没有被修改)。

术语:当读取寄存器的其余部分(或在某些其他情况下)时,所有这些都使AH(或DH)“脏”,即需要合并(使用合并的uop)。即,如果我正确理解这一点,那么AH将与RAX分开重命名。 “干净”恰恰相反。有很多方法可以清理脏寄存器,最简单的方法是使用emp或者eax,即esi。

仅写入AL:这些循环从LSD运行:uops_issue.any~ = lsd.uops。

>重复mov al,bl每循环1次运行。偶尔破坏xor eax,每组eax允许OOO执行瓶颈uop吞吐量,而不是延迟。
>重复mov al,[rsi]每循环运行1次,作为微融合ALU加载uop。 (uops_issued = 4G循环开销,uops_executed = 8G循环开销)。
一组破坏性的xor eax,eax在一组4之前让它在每个时钟的2个负载上出现瓶颈。
>重复mov al,123每循环1次运行。
>重复mov al,bh每循环运行0.5次。 (每2个循环1个)。阅读[ABCD] H很特别。
> xor eax,eax 6x mov al,bh dec ebp / jnz:2c per iter,前端每个时钟4 uop的瓶颈。
>重复添加dl,ch每循环运行0.5次。 (每2个循环1个)。读取[ABCD] H显然会为dl创建额外的延迟。
>重复添加dl,cl每循环运行1次。

我认为写入低8的注册表现为RMW混合到完整注册表中,就像添加eax一样,123会是,但如果啊是脏的话,它不会触发合并。所以(除了忽略AH合并之外)它的行为与完全不进行部分reg重命名cpu的行为相同。似乎AL永远不会与RAX分开重命名

> inc al / inc ah对可以并行运行。
> mov ecx,如果啊是“脏”的话,eax会插入一个合并的uop,但实际的mov会被重命名。这就是IvyBridge和之后的Agner Fog describes
>重复的movzx eax,啊每2个循环运行一次。 (在写完整个寄存器后读取高8位寄存器会产生额外的延迟。)
> movzx ecx,al具有零延迟,并且不在HSW和SKL上执行执行端口。 (就像Agner Fog为IvyBridge所描述的那样,但他说HSW不会重命名movzx)。
> movzx ecx,cl具有1c延迟并占用执行端口。 (mov-elimination never works for the same,same case,仅在不同的架构寄存器之间。)

每次迭代插入合并uop的循环都无法从LSD(循环缓冲区)运行?

我不认为AL / AH / RAX与B *,C *,DL / DH / RDX有什么特别之处。我已经在其他寄存器中测试了一些部分寄存器(尽管我主要是为了保持一致性而显示AL / AH),并且从未发现任何差异。

我们如何用一个关于微内部如何在内部工作的合理模型来解释所有这些观察结果?

相关:部分标记问题与部分寄存器问题不同。有关shr r32,cl(甚至是Core2 / Nehalem上的shr r32,2的一些超奇怪的东西,请参阅INC instruction vs ADD 1: Does it matter?:不要读取除1之外的移位标记)。

有关adc循环中的部分标记内容,另请参见Problems with ADC/SBB and INC/DEC in tight loops on some CPUs

其他答案欢迎更详细地介绍Sandybridge和IvyBridge。
我无法访问该硬件。

我没有发现HSW和SKL之间存在任何部分注册行为差异。
在Haswell和Skylake上,到目前为止我测试的所有内容支持这个模型:

AL永远不会与RAX(或r15中的r15b)分开重命名。因此,如果您从未触摸过high8寄存器(AH / BH / CH / DH),那么所有内容的行为与没有部分注册重命名cpu(例如AMD)完全相同。

对AL的只写访问权限合并到RAX中,并依赖于RAX。对于加载到AL的负载,这是一个微融合的ALU加载uop,它在p0156上执行,这是它在每次写入时真正合并的最有力的证据之一,而不仅仅是像Agner推测的那样进行一些花哨的双重记录。

Agner(和英特尔)称Sandybridge可能需要合并用于AL的uop,因此它可能与RAX分开重命名。对于SnB,Intel’s optimization manual (section 3.5.2.4 Partial Register Stalls)

SnB (not necessarily later uarches) inserts a merging uop in the following cases:

  • After a write to one of the registers AH,BH,CH or DH and before a
    following read of the 2-,4- or 8-byte form of the same register. In
    these cases a merge micro-op is inserted. The insertion consumes a
    full allocation cycle in which other micro-ops cannot be allocated.

  • After a micro-op with a destination register of 1 or 2 bytes,which is
    not a source of the instruction (or the register’s bigger form),and
    before a following read of a 2-,4- or 8-byte form of the same
    register. In these cases the merge micro-op is part of the flow.

我认为他们说在SnB上,添加al,bl将RMW完整的RAX而不是单独重命名,因为其中一个源寄存器是(部分)RAX。我的猜测是,这不适用于像mov al,[rbx rax];寻址模式下的rax可能不算作源。

我还没有测试过high8合并uops是否仍然需要在HSW / SKL上自行发布/重命名。这将使前端影响相当于4 uops(因为那是问题/重命名管道宽度)。

>如果不编写EAX / RAX,就无法打破涉及AL的依赖关系。 xor al,al没有帮助,mov al,0也没有。
> movzx ebx,al有zero latency (renamed),不需要执行单元。 (即,对HSW和SKL进行移动消除)。如果它很脏,它会触发AH的合并,我认为这是没有ALU工作所必需的。英特尔在引入mov-elimination的同一个uarch中降低了8的重命名,这可能并非巧合。 (Agner Fog的微型指南在这里有一个错误,他说在HSW或SKL上没有消除零扩展动作,只有IvB。)
> movzx eax,al在重命名时不会被删除。英特尔的mov-elimination永远不会同样适用。 mov rax,rax也没有被淘汰,即使它不需要对任何东西进行零扩展。 (虽然没有必要给它特殊的硬件支持,因为它只是一个无操作,不像mov eax,eax)。无论如何,当零扩展时,更喜欢在两个独立的架构寄存器之间移动,无论是32位mov还是8位movzx。
>在HSW或SKL上重命名时,不会删除> movzx eax,bx。它具有1c延迟并使用ALU uop。英特尔的优化手册仅提到了8位movzx的零延迟(并指出movzx r32,high8永远不会重命名)。

高8寄存器可以与寄存器的其余部分分开重命名,并且需要合并uop。

>只写访问啊用mov啊,r8或者mov啊,[mem]做重命名AH,不依赖于旧值。这些都是通常不需要ALU uop的指令(对于32位版本)。
>一个RM的AH(像公司啊)弄脏了它。
> setcc啊取决于老啊,但还是很脏。我觉得mov啊,imm8是一样的,但还没有测试过多少个角落的情况。

(不明原因:一个涉及setcc的循环啊有时候可以从LSD运行,请看这个帖子末尾的rcr循环。也许只要在循环结束时干净啊,它可以使用LSD吗?)。

如果啊是脏的,setcc啊合并到重命名啊,而不是强制合并到rax。例如%rep 4(包括/ test ebx,ebx / setcc啊/ inc al / inc啊)没有生成合并的uops,只能在大约8.7c内运行(因为来自uops的资源冲突,8的延迟减慢了8。还有inc啊/ setcc啊dep链)。

我想这里发生的事情是setcc r8总是被实现为读 – 修改 – 写。英特尔可能认为不值得使用只写setcc uop来优化setcc啊的情况,因为编译器生成代码很少用于setcc啊。 (但请看问题中的godbolt链接:clang4.0与-m32会这样做。)
>读取AX,EAX或RAX会触发合并uop(占用前端问题/重命名带宽)。可能RAT(寄存器分配表)跟踪架构R [ABCD] X的高8脏状态,甚至在写入AH退出之后,AH数据也存储在与RAX不同的物理寄存器中。即使在编写AH和读取EAX之间有256个NOP,也有一个额外的合并uop。 (SKL上的ROB大小= 224,所以这保证了mov ah,123已经退役)。使用uops_issued /执行的perf计数器检测到,这清楚地显示了差异。
>作为ALU uop的一部分,AL的读取 – 修改 – 写入(例如,inc)免费合并。 (仅使用一些简单的uops进行测试,例如add / inc,而不是div r8或mul r8)。同样,即使AH很脏,也不会触发合并的uop。
>只写EAX / RAX(如lea eax,[rsi rcx]或xor eax,eax)清除AH-dirty状态(不合并uop)。
>只写AX(mov ax,1)首先触发AH的合并。我想这不是特殊套管,而是像任何其他RMW AX / RAX一样运行。 (TODO:测试mov ax,bx,虽然这不应该是特殊的,因为它没有被重命名。)
> xor啊,啊有1c延迟,不是dep-breaking,还需要一个执行端口。
>读取和/或写入AL不会强制合并,因此AH可以保持脏(并且可以在单独的dep链中独立使用)。 (例如,添加啊,cl / add al,dl可以每个时钟运行1次(加密等待时间瓶颈)。

使AH变脏可防止循环从LSD(循环缓冲区)运行,即使没有合并的uop。 LSD是指cpu在队列中循环uops以提供问题/重命名阶段。 (称为IDQ)。

插入合并的uops有点像为堆栈引擎插入堆栈同步uops。英特尔的优化手册说,SnB的LSD无法运行具有不匹配的推/弹的循环,这是有道理的,但这意味着它可以运行具有平衡推/弹的循环。这不是我在SKL上看到的:即使是平衡的推/弹也阻止了从LSD运行(例如推rax / pop rdx /次6 imul rax,rdx。(SnB的LSD和HSW / SKL之间可能存在真正的区别: SnB may just “lock down” the uops in the IDQ instead of repeating them multiple times,so a 5-uop loop takes 2 cycles to issue instead of 1.25.)无论如何,当高8寄存器变脏或者包含堆栈引擎微操作时,似乎HSW / SKL不能使用LSD。

此行为可能与an erratum in SKL有关:

07005

Problem: Under complex micro-architectural conditions,short loops of less than 64 instruction that use AH,CH,or DH registers as well as their corresponding wider registers (e.g. RAX,EAX,or AX for AH) may cause unpredictable system behavIoUr. This can only happen when both logical processors on the same physical processor are active.

这也可能与英特尔的优化手册声明有关,即SnB至少必须在一个循环中自行发布/重命名AH合并uop。对于前端而言,这是一个奇怪的区别。

我的Linux内核日志说微码:sig = 0x506e3,pf = 0x2,revision = 0x84。
Arch Linux的intel-ucode软件包只提供更新,you have to edit config files to actually have it loaded。所以我的Skylake测试是在i7-6700k上进行微码修订版0x84,即doesn’t include the fix for SKL150.它与我测试的每一个案例中的Haswell行为相匹配,IIRC。 (例如Haswell和我的SKL都可以运行setne啊/ add啊,啊/ rcr ebx,1 / mov eax,ebx从LSD循环)。我启用了HT(这是SKL150显示的前提条件),但我正在测试一个大多数空闲的系统,所以我的线程有自己的核心。

使用更新的微码,LSD完全禁用所有时间,而不仅仅是部分寄存器处于活动状态。 lsd.uops总是正好为零,包括真正的程序而不是合成循环。硬件错误(而不是微码错误)通常需要禁用整个功能来修复。这就是为什么SKL-avx512(SKX)是reported to not have a loopback buffer.幸运的是,这不是性能问题:SKL在Broadwell上的uop-cache吞吐量增加几乎总能跟上问题/重命名

额外的AH / BH / CH / DH潜伏期:

>当它不脏时(单独重命名)读取AH会为两个操作数增加额外的延迟周期。例如添加bl,ah从输入BL到输出BL的延迟为2c,因此即使RAX和AH不是它的一部分,它也会增加关键路径的延迟。 (我之前看到过另一个操作数的这种额外延迟,在Skylake上有矢量延迟,其中一个int / float延迟会永久地“污染”一个寄存器.TODO:写下来。)

这意味着使用movzx ecx解压缩字节,al / movzx edx,ah与movzx / shr eax,8 / movzx有额外的延迟,但仍然有更好的吞吐量。

>在脏时读取AH不会增加任何延迟。 (加啊,啊还是加啊,dh / add dh,每次加1都有1c延迟)。在很多角落里,我没有做过很多测试来证实这一点。

假设:脏的high8值存储在物理寄存器的底部。读取干净的高电平8需要移位来提取位[15:8],但读取脏的高电平8只能取物理寄存器的位[7:0],就像正常的8位寄存器读取一样。

额外延迟并不意味着吞吐量降低。即使所有添加指令都有2c延迟(来自读取DH,未经修改),该程序每2个时钟可以运行1个iter。

global _start
_start:
    mov     ebp,100000000
.loop:
    add ah,dh
    add bh,dh
    add ch,dh
    add al,dh
    add bl,dh
    add cl,dh
    add dl,dh

    dec ebp
    jnz .loop

    xor edi,edi
    mov eax,231   ; __NR_exit_group  from /usr/include/asm/unistd_64.h
    syscall       ; sys_exit_group(0)@H_301_2@ 
 
Performance counter stats for './testloop':

     48.943652      task-clock (msec)         #    0.997 cpus utilized          
             1      context-switches          #    0.020 K/sec                  
             0      cpu-migrations            #    0.000 K/sec                  
             3      page-faults               #    0.061 K/sec                  
   200,314,806      cycles                    #    4.093 GHz                    
   100,024,930      branches                  # 2043.675 M/sec                  
   900,136,527      instructions              #    4.49  insn per cycle         
   800,219,617      uops_issued_any           # 16349.814 M/sec                 
   800,014      uops_executed_thread      # 16349.802 M/sec                 
         1,903      lsd_uops                  #    0.039 M/sec                  

   0.049107358 seconds time elapsed@H_301_2@ 
 

一些有趣的测试循环体:

%if 1
     imul eax,eax
     mov  dh,al
     inc dh
     inc dh
     inc dh
;     add al,dl
    mov cl,dl
    movzx eax,cl
%endif

Runs at ~2.35c per iteration on both HSW and SKL.  reading `dl` has no dep on the `inc dh` result.  But using `movzx eax,dl` instead of `mov cl,dl` / `movzx eax,cl` causes a partial-register merge,and creates a loop-carried dep chain.  (8c per iteration).


%if 1
    imul  eax,eax
    imul  eax,eax         ; off the critical path unless there's a false dep

  %if 1
    test  ebx,ebx          ; independent of the imul results
    ;mov   ah,123         ; dependent on RAX
    ;mov  eax,0           ; breaks the RAX dependency
    setz  ah              ; dependent on RAX
  %else
    mov   ah,bl          ; dep-breaking
  %endif

    add   ah,ah
    ;; ;inc   eax
;    sbb   eax,eax

    rcr   ebx,1      ; dep on  add ah,ah  via CF
    mov   eax,ebx     ; clear AH-dirty

    ;; mov   [rdi],ah
    ;; movzx eax,byte [rdi]   ; clear AH-dirty,and remove dep on old value of RAX
    ;; add   ebx,eax          ; make the dep chain through AH loop-carried
%endif@H_301_2@ 
 

setcc版本(带%if 1)有20c循环延迟,并且从LSD运行即使它有setcc啊加啊,啊。

00000000004000e0 <_start.loop>:
  4000e0:       0f af c0                imul   eax,eax
  4000e3:       0f af c0                imul   eax,eax
  4000e6:       0f af c0                imul   eax,eax
  4000e9:       0f af c0                imul   eax,eax
  4000ec:       0f af c0                imul   eax,eax
  4000ef:       85 db                   test   ebx,ebx
  4000f1:       0f 94 d4                sete   ah
  4000f4:       00 e4                   add    ah,ah
  4000f6:       d1 db                   rcr    ebx,1
  4000f8:       89 d8                   mov    eax,ebx
  4000fa:       ff cd                   dec    ebp
  4000fc:       75 e2                   jne    4000e0 <_start.loop>

 Performance counter stats for './testloop' (4 runs):

       4565.851575      task-clock (msec)         #    1.000 cpus utilized            ( +-  0.08% )
                 4      context-switches          #    0.001 K/sec                    ( +-  5.88% )
                 0      cpu-migrations            #    0.000 K/sec                  
                 3      page-faults               #    0.001 K/sec                  
    20,007,739,240      cycles                    #    4.382 GHz                      ( +-  0.00% )
     1,001,181,788      branches                  #  219.276 M/sec                    ( +-  0.00% )
    12,006,455,028      instructions              #    0.60  insn per cycle           ( +-  0.00% )
    13,009,415,501      uops_issued_any           # 2849.286 M/sec                    ( +-  0.00% )
    12,592,328      uops_executed_thread      # 2630.307 M/sec                    ( +-  0.00% )
    13,055,852,774      lsd_uops                  # 2859.456 M/sec                    ( +-  0.29% )

       4.565914158 seconds time elapsed                                          ( +-  0.08% )@H_301_2@ 
 

不明原因:它从LSD运行,即使它使AH变脏。 (至少我认为确实如此.TODO:尝试在mov eax之前添加一些与eax做某事的指令,ebx清除它。)

但是对于mov啊,bl,它在HSW / SKL上每次迭代运行5.0c(imul吞吐量瓶颈)。 (已注释掉的商店/重装也有效,但SKL的存储转发速度比HSW快,而且是variable-latency …)

#  mov ah,bl   version
 5,785,393      cycles                    #    4.289 GHz                      ( +-  0.08% )
 1,000,315,930      branches                  #  856.373 M/sec                    ( +-  0.00% )
11,728,338      instructions              #    2.20  insn per cycle           ( +-  0.00% )
12,003,708      uops_issued_any           # 10275.807 M/sec                   ( +-  0.00% )
11,002,974,066      uops_executed_thread      # 9419.678 M/sec                    ( +-  0.00% )
         1,806      lsd_uops                  #    0.002 M/sec                    ( +-  3.88% )

   1.168238322 seconds time elapsed                                          ( +-  0.33% )@H_301_2@ 
 

请注意,它不再从LSD运行。

原文链接:https://www.f2er.com/javaschema/282065.html

猜你在找的设计模式相关文章