热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

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

如何解决《Haswell/Skylake的部分寄存器究竟如何表现?写AL似乎对RAX有假依赖,而AH是不一致的》经验,为你挑选了1个好方法。

此循环在英特尔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);

如果setnz al依赖于rax,则3ximul/setcc/movzx序列形成循环携带的依赖链.如果没有,每个setcc/ movzx/ 3x imul链是独立的,从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延迟.它还在CPU上使用少一个ALU uop来处理寄存器重命名阶段的xor-zeroing.有关使用xor-zeroing的更多信息,请参阅该链接setcc.

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

GCC遗憾的是并倾向于使用cmp/ setcc al/ movzx eax,al它也可以使用xor,而不是movzx (Godbolt编译器资源管理器的例子),而铛使用XOR零/ CMP/setcc除非你把喜欢多个布尔条件count += (a==b) | (a==~b).

xor/dec/setnz版本在Skylake,Haswell和Core2上每次迭代运行3.0c(imul吞吐量瓶颈). xor-zeroing打破了对eax除PPro/PII/PIII /早期Pentium-M之外的所有无序CPU 的旧值的依赖性(它仍然避免了部分寄存器合并处罚但不会破坏dep). Agner Fog的微型指南描述了这一点.更换xor- zeroing并将mov eax,0其降低到Core2上每4.78个循环减速一次:2-3c失速(在前端?),以便在imul读取eax后插入部分reg合并uopsetnz al.

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


Core2行为与Agner Fog的微观指南一致,但HSW/SKL行为并非如此.从第11.10节到Skylake,以及之前的英特尔搜索:

通用寄存器的不同部分可以存储在不同的临时寄存器中,以消除错误的依赖性.

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

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


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

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

英特尔的优化手册并没有具体说明哪些CPU具有错误依赖性(虽然它确实提到某些CPU具有它们),并且省略了诸如读取AH/BH/CH/DH(high8寄存器)之类的东西,即使它们没有它们也会增加额外的延迟没被修改过.

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


我的Skylake测试数据,将%rep 4短序列放入一个dec ebp/jnz运行100M或1G迭代的小循环中.我用Linux测试循环perf的方式与我在这里的答案相同,在相同的硬件上(桌面Skylake i7 6700k).

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

"每循环4个"案例是对无限展开案例的推断.循环开销占用了一些前端带宽,但是每个周期优于1的任何东西都表明寄存器重命名避免了写后写输出依赖性,并且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 ah, [rsi]每个周期重复运行2次(负载吞吐量瓶颈).

mov ah, 123每个循环重复运行1次.(循环内部的破坏xor eax,eax消除了瓶颈.)

重复setz ahsetc ah每循环1次运行.(一个dep-breaking xor eax,eax让它成为p06吞吐量setcc和循环分支的瓶颈.)

为什么ah用通常使用ALU执行单元的指令写入对旧值有错误依赖,而mov r8, r/m8对于reg或内存src却没有? (那又怎么样mov r/m8, r8?你使用reg-reg的两个操作码中的哪一个无关紧要?)

add ah, 123正如预期的那样,每个周期重复运行1次.

add dh, cl每个循环重复运行1次.

add dh, dh每个循环重复运行1次.

add dh, ch每循环重复运行0.5次.读取[ABCD] H在它们"干净"时是特殊的(在这种情况下,RCX最近根本没有被修改).

术语:所有这些都使AH(或DH)" ",即在读取其余寄存器时(或在某些其他情况下)需要合并(使用合并的uop).即如果我正确理解这一点,那么AH将与RAX分开重命名." 干净 "恰恰相反.有许多方法来清洗脏的寄存器,其中最简单的inc eaxmov 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,前端每个时钟4个uop的瓶颈.

add dl, ch每循环重复运行0.5次.(每2个循环1个).阅读[ABCD] H显然会产生额外的延迟dl.

add dl, cl每个循环重复运行1次.

我认为对低8 reg的写入表现为RMW混合到完整的reg中,就像add eax, 123它一样,但如果ah是脏的话它不会触发合并.所以(除了忽略AH合并之外)它的行为与完全不进行部分reg重命名的CPU的行为相同.它似乎AL从未单独重命名RAX

inc al/ inc ahpairs可以并行运行.

mov ecx, eax如果ah是"脏",则插入合并的uop ,但实际mov重命名.这就是Agner Fog为IvyBridge和后来描述的内容.

movzx eax, ah每2个循环重复一次.(在写完整个reg之后读取高8位寄存器会产生额外的延迟.)

movzx ecx, al没有延迟,并且不在HSW和SKL上执行执行端口.(就像Agner Fog为IvyBridge所描述的那样,但他说HSW不会重命名movzx).

movzx ecx, cl具有1c延迟并占用执行端口.(mov-elimination永远不适用于same,same案例,仅适用于不同的架构寄存器.)

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

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

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


相关:部分标记问题与部分注册问题不同.请参阅INC指令与ADD 1:重要吗?对于一些非常奇怪的东西shr r32,cl(甚至shr r32,2在Core2/Nehalem上:不要从1以外的移位中读取标记).

另请参阅某些CPU上的紧密循环中的ADC/SBB和INC/DEC问题,以了解循环中的部分标志内容adc.



1> Peter Cordes..:

其他答案欢迎更详细地介绍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,英特尔的优化手册(第3.5.2.4节"部分寄存器停顿")说

在以下情况下,SnB(不一定是后来的搜索)会插入合并的uop:

在写入寄存器AH,BH,CH或DH之一之后并且在随后读取相同寄存器的2字节,4字节或8字节形式之前.在这些情况下,插入合并微操作.插入消耗完整的分配周期,其中不能分配其他微操作.

在具有1或2个字节的目标寄存器的微操作之后,该寄存器不是指令的源(或寄存器的更大形式),并且在随后读取2字节,4字节或8字节形式的相同的注册.在这些情况下,合并微操作是流程的一部分.

我认为他们说在SnB上,add al,blRMW将完整的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具有零延迟(重命名),并且不需要执行单元. (即,移动消除适用于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.

movzx eax, bx不是在对HSW或SKL重命名消除.它具有1c延迟并使用ALU uop.英特尔的优化手册仅提到了8位movzx的零延迟(并指出movzx r32, high8永远不会重命名).


高8寄存器可以与寄存器的其余部分分开重命名,并且需要合并微操作.

只写访问ahmov ah, reg8mov ah, [mem8]做重命名AH,与旧值不存在依赖关系.这些都是通常不需要ALU uop的指令(对于32位版本).

一个RM的AH(喜欢mov ah, bl)弄脏了它.

inc ah取决于旧的setcc ah,但仍然很脏.我认为ah是相同的,但没有测试过多的角落情况.

(原因不明:涉及循环mov ah, imm8有时可以从LSD运行,请参阅本文setcc ah末尾的循环.也许只要循环结束时它rcr是干净的,它可以使用LSD吗?).

如果ah是脏的,则ah合并到重命名的setcc ah,而不是强制合并ah.例如 rax(%rep 4/ inc al/ test ebx,ebx/ setcc ah/ inc al)不生成合并的uops,并且只运行大约8.7c(inc ah由于来自uops 的资源冲突,8的延迟减慢inc al.还有ah/ inc ahdep链).

我认为这里发生的事情setcc ah始终是作为读 - 修改 - 写实现的.英特尔可能认为不值得使用只写setcc r8uop来优化setcc案例,因为编译器生成的代码非常少见setcc ah.(但请看问题中的godbolt链接:clang4.0 setcc ah会这样做.)

读取AX,EAX或RAX会触发合并uop(占用前端问题/重命名带宽).可能RAT(寄存器分配表)跟踪架构R [ABCD] X的高8脏状态,甚至在写入AH退出之后,AH数据也存储在与RAX不同的物理寄存器中.即使在编写AH和读取EAX之间有256个NOP,也有一个额外的合并uop.(SKL上的ROB大小= 224,因此这保证了它-m32已经退役).使用uops_issued /执行的perf计数器检测到,这清楚地显示了差异.

AL的读取 - 修改 - 写入(例如mov ah, 123)免费合并,作为ALU uop的一部分.(仅使用一些简单的uops进行测试,例如inc al/ add,不是incdiv r8).同样,即使AH很脏,也不会触发合并的uop.

只写到EAX/RAX(像mul r8lea eax, [rsi + rcx])清除AH脏状态(没有合并的uop).

只写AX(xor eax,eax)首先触发AH的合并.我想这不是特殊套管,而是像任何其他RMW AX/RAX一样运行.(TODO:测试mov ax, 1,虽然这不应该是特殊的,因为它没有被重命名.)

mov ax, bx 有1c延迟,不是破坏,仍然需要一个执行端口.

读取和/或写入AL不会强制合并,因此AH可以保持脏(并且可以在单独的dep链中独立使用).(例如xor ah,ah/ add ah, cl可以每个时钟运行1次(增加延迟的瓶颈).


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

插入合并的uops有点像为堆栈引擎插入堆栈同步uops.英特尔的优化手册说SnB的LSD不能运行不匹配的循环add al, dl/ push,这是有道理的,但这意味着它可以运行带有平衡pop/的循环push.这不是我所看到的SKL:即使平衡pop/ push阻止来自LSD(运行如pop/ push rax/ pop rdx(有可能是瑞士央行的LSD和HSW/SKL之间的真正区别: 在IDQ SNB可能只是"锁定"的微指令而不是多次重复它们,所以5-uop循环需要2个周期而不是1.25.)无论如何,当高8寄存器变脏或者包含高速8时,HSW/SKL似乎不能使用LSD.堆栈引擎uops.

此行为可能与SKL中的错误相关:

SKL150:使用AH/BH/CH/DH寄存器的短循环可能导致不可预测的系统行为

问题:在复杂的微架构条件下,使用AH,BH,CH或DH寄存器以及相应的较宽寄存器(例如AH的RAX,EAX或AX)的小于64指令的短循环可能会导致不可预测的系统行为.这只能在同一物理处理器上的两个逻辑处理器都处于活动状态时发生.

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

我的Linux内核日志说times 6 imul rax, rdx.Arch Linux的microcode: sig=0x506e3, pf=0x2, revision=0x84软件包只提供更新,你必须编辑配置文件以实际加载它.所以我的Skylake测试是在i7-6700k上进行,微码修订版为0x84,不包括SKL150的修复程序.在我测试的每一个案例中,它都符合Haswell的行为,IIRC.(例如,Haswell和我的SKL都可以从LSD 运行intel-ucode/ setne ah/ add ah,ah/ rcr ebx,1循环).我启用了HT(这是SKL150显示的前提条件),但我正在测试一个主要是空闲的系统,所以我的线程有自己的核心.

使用更新的微码,LSD完全禁用所有时间,而不仅仅是部分寄存器处于活动状态. mov eax,ebx总是精确为零,包括真正的程序而不是合成循环.硬件错误(而不是微码错误)通常需要禁用整个功能来修复.这就是据报道 SKL-avx512(SKX)没有环回缓冲区的原因.幸运的是,这不是性能问题:SKL在Broadwell上增加的uop-cache吞吐量几乎总能跟上问题/重命名.


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

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

这意味着拆包个字节add bl, ah/ movzx ecx, al有额外的延迟与movzx edx, ah/ movzx/ shr eax,8,但还是更好的吞吐量.

阅读AH当它脏不添加任何延迟.(movzxadd ah,ah/ add ah,dh每次添加1c延迟).在很多角落里,我没有做过很多测试来证实这一点.

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

额外延迟并不意味着吞吐量降低.即使所有add dh,ah指令都有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)

 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,219,014      uops_executed_thread      # 16349.802 M/sec                 
         1,903      lsd_uops                  #    0.039 M/sec                  

   0.049107358 seconds time elapsed

一些有趣的测试循环体:

%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
    imul  eax, eax
    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

setcc版本(带有add)具有20c循环传输延迟,并且从LSD运行,即使它具有%if 1setcc ah.

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,009,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% )

不明原因:它从LSD运行,即使它使AH变脏.(至少我认为确实如此.TODO:尝试add ah,aheax清除之前添加一些与之相关的指令.)

但是mov eax,ebx,它在mov ah, blHSW/SKL上每次迭代运行5.0c(吞吐量瓶颈).(已注释掉的商店/重新加载也可以,但SKL的存储转发速度比HSW快,而且可变延迟 ......)

 #  mov ah, bl   version
 5,009,785,393      cycles                    #    4.289 GHz                      ( +-  0.08% )
 1,000,315,930      branches                  #  856.373 M/sec                    ( +-  0.00% )
11,001,728,338      instructions              #    2.20  insn per cycle           ( +-  0.00% )
12,003,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% )

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


这是[x86]调查性新闻报道的最佳状态.谢谢!
推荐阅读
author-avatar
sendymylove睛飘益
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有