在.Net 7性能改进-栈上替换(OSR)
liebian365 2024-11-27 17:08 2 浏览 0 评论
前言
本文是Performance Improvements in .NET 7 OSR部分的翻译.下面开始正文:
//原文地址: https://devblogs.microsoft.com/dotnet/performance_improvements_in_net_7/#loop-hoisting-and-cloning
On-Stack Replacement(栈上替换),是在.NET 7中实现JIT的最酷的功能之一.但要真正理解OSR,我们首先需要理解分层编译,所以快速回顾一下…
使用JIT编译器的托管环境必须处理的问题之一是启动和吞吐量之间的权衡.从历史上看,编译器优化的任务是生成执行更快的代码,以便在应用程序或服务运行时实现尽可能最佳的吞吐量.但这种优化需要分析,需要时间,执行所有这些工作会导致启动时间增加,因为程序的所有代码(例如,在web服务器可以服务第一个请求之前需要运行的所有代码)都需要编译.因此,JIT编译器需要做出权衡:以更长的启动时间为代价提高吞吐量,或以降低吞吐量为代价提高启动时间.对于某些类型的应用程序和服务,折衷是一个简单的调用,例如,如果您的服务启动一次,然后运行几天,额外几秒的启动时间并不重要,或者如果您是一个控制台应用程序,将要进行快速计算并退出,启动时间才是最重要的.
但是,JIT如何知道它处于哪个场景中,我们真的希望每个开发人员都知道这些设置和权衡,并相应地配置他们的每个应用程序吗?对此的一个答案是提前编译,它在.NET中采用了多种形式.例如,所有的核心库都是“crossgen”,这意味着它们已经通过一个工具运行,该工具生成了前面提到的R2R格式,生成的二进制文件包含汇编代码,只需要稍加调整即可实际执行;不是每个方法都可以为其生成代码,但足以显著减少启动时间.当然,这种方法也有其自身的缺点,例如,JIT编译器的一个承诺是,它可以利用当前机器/进程的知识进行最佳优化,因此,例如,R2R映像必须假设某个基线指令集(例如,哪些矢量化指令可用),而JIT可以看到哪些实际可用并使用最佳.“分层编译”提供了另一个答案,无论是否使用这些其他提前(AOT)编译解决方案,都可以使用.
分层编译使JIT能够鱼与熊掌兼得.分层编译这个想法很简单:允许JIT多次编译相同的代码.第一次,JIT可以使用尽可能少的优化(少量优化实际上可以使JIT自身的吞吐量更快,因此这些优化仍然适用),生成相当未优化的汇编代码,但速度非常快.当它这样做时,它可以在程序集中添加一些工具来跟踪方法的调用频率.事实证明,在启动路径上使用的许多函数被调用一次,或者可能只调用了几次,优化它们比不优化执行它们需要更多的时间.然后,当方法的插装触发某个阈值时,例如,一个方法已执行30次,工作项将排队重新编译该方法,但这一次JIT可以对其进行所有优化.这被亲切地称为“分层”.一旦重新编译完成,方法的调用站点就会用新高度优化的汇编代码的地址进行修补,未来的调用将采用快速路径.因此,我们获得了更快的启动和更快的持续吞吐量.
然而,一个问题是不适合这种模式的方法.当然,许多性能敏感的方法都相对较快,执行了很多次,但也有大量性能敏感方法只执行了几次,甚至可能只执行一次,但执行需要很长时间,甚至可能是整个过程的持续时间:带循环的方法.因此,默认情况下,分层编译未应用于循环,但可以通过将DOTNET_TC_QuickJitForLoops环境变量设置为1来启用.我们可以通过尝试使用.NET 6的简单控制台应用程序来查看其效果.使用默认设置,运行此应用程序:
class Program
{
static void Main()
{
var sw = new System.Diagnostics.Stopwatch();
while (true)
{
sw.Restart();
for (int trial = 0; trial < 10_000; trial++)
{
int count = 0;
for (int i = 0; i < char.MaxValue; i++)
if (IsAsciiDigit((char)i))
count++;
}
sw.Stop();
Console.WriteLine(sw.Elapsed);
}
static bool IsAsciiDigit(char c) => (uint)(c - '0') <= 9;
}
}
输出内容:
00:00:00.5734352
00:00:00.5526667
00:00:00.5675267
00:00:00.5588724
00:00:00.5616028
现在,尝试将DOTNET_TC_QuickJitForLoops设置为1.当我再次运行它时,得到如下数字:
00:00:01.2841397
00:00:01.2693485
00:00:01.2755646
00:00:01.2656678
00:00:01.2679925
换句话说,启用DOTNET_TC_QuickJitForLoops时,需要的时间是不启用时的2.5倍(在.NET6中).这是因为这个主函数从未应用过优化.通过将DOTNET_TC_QuickJitForLoops设置为1,我们说“JIT,请将分层也应用于具有循环的方法”,但这种具有循环的方式只调用一次,因此在整个过程中,它最终保持在“tier-0”,即未优化.现在,让我们对.NET 7进行同样的尝试.无论是否设置了环境变量,我都会再次得到如下数字:
00:00:00.5528889
00:00:00.5562563
00:00:00.5622086
00:00:00.5668220
00:00:00.5589112
但重要的是,这种方法仍然参与了分层.事实上,我们可以通过使用前面提到的DOTNET_JitDisasmSummary=1环境变量来确认.当我设置并再次运行时,我在输出中看到这些行:
4: JIT compiled Program:Main() [Tier0, IL size=83, code size=319]
...
6: JIT compiled Program:Main() [Tier1-OSR @0x27, IL size=83, code size=380]
栈上替换的思想是,方法不仅可以在调用之间替换,而且可以在“栈上”执行时替换.除了为调用计数检测第0层代码外,还为迭代计数检测循环.当迭代超过某个限制时,JIT编译该方法的新的高度优化版本,将所有本地/寄存器状态从当前调用转移到新调用,然后跳到新方法中的适当位置.通过使用前面讨论的DOTNET_JitDisasm环境变量,我们可以看到这一点.将其设置为Program:*以查看为Program类中的所有方法生成的汇编代码,然后再次运行应用程序.您应该看到如下输出:
// Assembly listing for method Program:Main()
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-0 compilation
// MinOpts code
// rbp based frame
// partially interruptible
G_M000_IG01: ;; offset=0000H
55 push rbp
4881EC80000000 sub rsp, 128
488DAC2480000000 lea rbp, [rsp+80H]
C5D857E4 vxorps xmm4, xmm4
C5F97F65B0 vmovdqa xmmword ptr [rbp-50H], xmm4
33C0 xor eax, eax
488945C0 mov qword ptr [rbp-40H], rax
G_M000_IG02: ;; offset=001FH
48B9002F0B50FC7F0000 mov rcx, 0x7FFC500B2F00
E8721FB25F call CORINFO_HELP_NEWSFAST
488945B0 mov gword ptr [rbp-50H], rax
488B4DB0 mov rcx, gword ptr [rbp-50H]
FF1544C70D00 call [Stopwatch:.ctor():this]
488B4DB0 mov rcx, gword ptr [rbp-50H]
48894DC0 mov gword ptr [rbp-40H], rcx
C745A8E8030000 mov dword ptr [rbp-58H], 0x3E8
G_M000_IG03: ;; offset=004BH
8B4DA8 mov ecx, dword ptr [rbp-58H]
FFC9 dec ecx
894DA8 mov dword ptr [rbp-58H], ecx
837DA800 cmp dword ptr [rbp-58H], 0
7F0E jg SHORT G_M000_IG05
G_M000_IG04: ;; offset=0059H
488D4DA8 lea rcx, [rbp-58H]
BA06000000 mov edx, 6
E8B985AB5F call CORINFO_HELP_PATCHPOINT
G_M000_IG05: ;; offset=0067H
488B4DC0 mov rcx, gword ptr [rbp-40H]
3909 cmp dword ptr [rcx], ecx
FF1585C70D00 call [Stopwatch:Restart():this]
33C9 xor ecx, ecx
894DBC mov dword ptr [rbp-44H], ecx
33C9 xor ecx, ecx
894DB8 mov dword ptr [rbp-48H], ecx
EB20 jmp SHORT G_M000_IG08
G_M000_IG06: ;; offset=007FH
8B4DB8 mov ecx, dword ptr [rbp-48H]
0FB7C9 movzx rcx, cx
FF152DD40B00 call [Program:<Main>g__IsAsciiDigit|0_0(ushort):bool]
85C0 test eax, eax
7408 je SHORT G_M000_IG07
8B4DBC mov ecx, dword ptr [rbp-44H]
FFC1 inc ecx
894DBC mov dword ptr [rbp-44H], ecx
G_M000_IG07: ;; offset=0097H
8B4DB8 mov ecx, dword ptr [rbp-48H]
FFC1 inc ecx
894DB8 mov dword ptr [rbp-48H], ecx
G_M000_IG08: ;; offset=009FH
8B4DA8 mov ecx, dword ptr [rbp-58H]
FFC9 dec ecx
894DA8 mov dword ptr [rbp-58H], ecx
837DA800 cmp dword ptr [rbp-58H], 0
7F0E jg SHORT G_M000_IG10
G_M000_IG09: ;; offset=00ADH
488D4DA8 lea rcx, [rbp-58H]
BA23000000 mov edx, 35
E86585AB5F call CORINFO_HELP_PATCHPOINT
G_M000_IG10: ;; offset=00BBH
817DB800CA9A3B cmp dword ptr [rbp-48H], 0x3B9ACA00
7CBB jl SHORT G_M000_IG06
488B4DC0 mov rcx, gword ptr [rbp-40H]
3909 cmp dword ptr [rcx], ecx
FF1570C70D00 call [Stopwatch:get_ElapsedMilliseconds():long:this]
488BC8 mov rcx, rax
FF1507D00D00 call [Console:WriteLine(long)]
E96DFFFFFF jmp G_M000_IG03
// Total bytes of code 222
// Assembly listing for method Program:<Main>g__IsAsciiDigit|0_0(ushort):bool
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-0 compilation
// MinOpts code
// rbp based frame
// partially interruptible
G_M000_IG01: ;; offset=0000H
55 push rbp
488BEC mov rbp, rsp
894D10 mov dword ptr [rbp+10H], ecx
G_M000_IG02: ;; offset=0007H
8B4510 mov eax, dword ptr [rbp+10H]
0FB7C0 movzx rax, ax
83C0D0 add eax, -48
83F809 cmp eax, 9
0F96C0 setbe al
0FB6C0 movzx rax, al
G_M000_IG03: ;; offset=0019H
5D pop rbp
C3 ret
这里有一些相关的事情需要注意.首先,顶部的注释强调了这段代码是如何编译的:
// Tier-0 compilation
// MinOpts code
因此,我们知道这是用最小优化(“MinOpts”)编译的方法的初始版本(“第0层”).第二,注意装配的这一行:
FF152DD40B00 call [Program:<Main>g__IsAsciiDigit|0_0(ushort):bool]
我们的IsAsciiDigit辅助方法是简单的可内联的,但它没有内联;相反,程序集有一个对它的调用,事实上,我们可以在下面看到为IsAsciiDigit生成的代码(也是“MinOpts”).为什么?因为内联是一种优化(非常重要的优化),但在tier-0中被禁用(因为为了进行良好的内联分析也非常昂贵).第三,我们可以看到JIT输出到检测该方法的代码.这有点复杂,但我会指出相关部分.首先,我们看到:
C745A8E8030000 mov dword ptr [rbp-58H], 0x3E8
0x3E8是十进制1000的十六进制值,这是在JIT生成方法的优化版本之前循环需要迭代的默认迭代次数(可通过环境变量DOTNET_TC_OnStackReplacement_InitialCounter进行配置).因此,我们看到1000存储在这个堆栈位置.然后,在该方法的后面,我们看到:
G_M000_IG03: // offset=004BH
8B4DA8 mov ecx, dword ptr [rbp-58H]
FFC9 dec ecx
894DA8 mov dword ptr [rbp-58H], ecx
837DA800 cmp dword ptr [rbp-58H], 0
7F0E jg SHORT G_M000_IG05
G_M000_IG04: // offset=0059H
488D4DA8 lea rcx, [rbp-58H]
BA06000000 mov edx, 6
E8B985AB5F call CORINFO_HELP_PATCHPOINT
G_M000_IG05: // offset=0067H
生成的代码将该计数器加载到ecx寄存器中,将其递减,存储回,然后查看计数器是否下降到0.如果没有下降,则代码跳转到G_M000_IG05,这是循环其余部分中实际代码的标签.但如果计数器下降到0,JIT将继续将相关状态存储到rcx和edx寄存器中,然后调用CORINFO_HELP_PATCHPOINT helper方法.该助手负责触发优化方法的创建(如果它还不存在),修复所有适当的跟踪状态,并跳转到新方法.事实上,如果您再次查看运行程序的控制台输出,您将看到主方法的另一个输出:
// Assembly listing for method Program:Main()
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-1 compilation
// OSR variant for entry point 0x23
// optimized code
// rsp based frame
// fully interruptible
// No PGO data
// 1 inlinees with PGO data; 8 single block inlinees; 0 inlinees without PGO data
G_M000_IG01: // offset=0000H
4883EC58 sub rsp, 88
4889BC24D8000000 mov qword ptr [rsp+D8H], rdi
4889B424D0000000 mov qword ptr [rsp+D0H], rsi
48899C24C8000000 mov qword ptr [rsp+C8H], rbx
C5F877 vzeroupper
33C0 xor eax, eax
4889442428 mov qword ptr [rsp+28H], rax
4889442420 mov qword ptr [rsp+20H], rax
488B9C24A0000000 mov rbx, gword ptr [rsp+A0H]
8BBC249C000000 mov edi, dword ptr [rsp+9CH]
8BB42498000000 mov esi, dword ptr [rsp+98H]
G_M000_IG02: // offset=0041H
EB45 jmp SHORT G_M000_IG05
align [0 bytes for IG06]
G_M000_IG03: // offset=0043H
33C9 xor ecx, ecx
488B9C24A0000000 mov rbx, gword ptr [rsp+A0H]
48894B08 mov qword ptr [rbx+08H], rcx
488D4C2428 lea rcx, [rsp+28H]
48B87066E68AFD7F0000 mov rax, 0x7FFD8AE66670
G_M000_IG04: // offset=0060H
FFD0 call rax ; Kernel32:QueryPerformanceCounter(long):int
488B442428 mov rax, qword ptr [rsp+28H]
488B9C24A0000000 mov rbx, gword ptr [rsp+A0H]
48894310 mov qword ptr [rbx+10H], rax
C6431801 mov byte ptr [rbx+18H], 1
33FF xor edi, edi
33F6 xor esi, esi
833D92A1E55F00 cmp dword ptr [(reloc 0x7ffcafe1ae34)], 0
0F85CA000000 jne G_M000_IG13
G_M000_IG05: // offset=0088H
81FE00CA9A3B cmp esi, 0x3B9ACA00
7D17 jge SHORT G_M000_IG09
G_M000_IG06: // offset=0090H
0FB7CE movzx rcx, si
83C1D0 add ecx, -48
83F909 cmp ecx, 9
7702 ja SHORT G_M000_IG08
G_M000_IG07: // offset=009BH
FFC7 inc edi
G_M000_IG08: // offset=009DH
FFC6 inc esi
81FE00CA9A3B cmp esi, 0x3B9ACA00
7CE9 jl SHORT G_M000_IG06
G_M000_IG09: // offset=00A7H
488B6B08 mov rbp, qword ptr [rbx+08H]
48899C24A0000000 mov gword ptr [rsp+A0H], rbx
807B1800 cmp byte ptr [rbx+18H], 0
7436 je SHORT G_M000_IG12
G_M000_IG10: // offset=00B9H
488D4C2420 lea rcx, [rsp+20H]
48B87066E68AFD7F0000 mov rax, 0x7FFD8AE66670
G_M000_IG11: // offset=00C8H
FFD0 call rax ; Kernel32:QueryPerformanceCounter(long):int
488B4C2420 mov rcx, qword ptr [rsp+20H]
488B9C24A0000000 mov rbx, gword ptr [rsp+A0H]
482B4B10 sub rcx, qword ptr [rbx+10H]
4803E9 add rbp, rcx
833D2FA1E55F00 cmp dword ptr [(reloc 0x7ffcafe1ae34)], 0
48899C24A0000000 mov gword ptr [rsp+A0H], rbx
756D jne SHORT G_M000_IG14
G_M000_IG12: // offset=00EFH
C5F857C0 vxorps xmm0, xmm0
C4E1FB2AC5 vcvtsi2sd xmm0, rbp
C5FB11442430 vmovsd qword ptr [rsp+30H], xmm0
48B9F04BF24FFC7F0000 mov rcx, 0x7FFC4FF24BF0
BAE7070000 mov edx, 0x7E7
E82E1FB25F call CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE
C5FB10442430 vmovsd xmm0, qword ptr [rsp+30H]
C5FB5905E049F6FF vmulsd xmm0, xmm0, qword ptr [(reloc 0x7ffc4ff25720)]
C4E1FB2CD0 vcvttsd2si rdx, xmm0
48B94B598638D6C56D34 mov rcx, 0x346DC5D63886594B
488BC1 mov rax, rcx
48F7EA imul rdx:rax, rdx
488BCA mov rcx, rdx
48C1E93F shr rcx, 63
48C1FA0B sar rdx, 11
4803CA add rcx, rdx
FF1567CE0D00 call [Console:WriteLine(long)]
E9F5FEFFFF jmp G_M000_IG03
G_M000_IG13: // offset=014EH
E8DDCBAC5F call CORINFO_HELP_POLL_GC
E930FFFFFF jmp G_M000_IG05
G_M000_IG14: // offset=0158H
E8D3CBAC5F call CORINFO_HELP_POLL_GC
EB90 jmp SHORT G_M000_IG12
// Total bytes of code 351
这里,我们再次注意到一些有趣的事情.首先,在头文件中我们看到:
// Tier-1 compilation
// OSR variant for entry point 0x23
// optimized code
因此,我们知道这既是优化的“第1层”代码,也是该方法的“OSR变体”.其次,请注意,不再调用IsAsciiDigit方法.相反,我们看到的是,该调用的位置:
G_M000_IG06: ;; offset=0090H
0FB7CE movzx rcx, si
83C1D0 add ecx, -48
83F909 cmp ecx, 9
7702 ja SHORT G_M000_IG08
这是将一个值加载到rcx中,从中减去48(48是“0”字符的十进制ASCII值),并将结果值与9进行比较.听起来很像我们的IsasciidGit实现( (uint)(c-“0”)<=9 ),不是吗?这是因为它是.帮助程序成功地内联到现在优化的代码中.
很好,现在在.NET7中,我们可以在很大程度上避免启动和吞吐量之间的权衡,因为OSR使分层编译能够应用于所有方法,甚至是那些长期运行的方法.许多提交开始启用此功能,包括过去几年中的许多提交,但是所有的功能在发布时都被禁用了.由于dotnet/runtime#62831在Arm64上实现了对OSR的支持(之前仅实现了x64支持),以及dotnet/Runtime#63406和dotnet/runtime#65609修改了如何OSR导入和epilog的处理,dotnet/runtime#65675在默认情况下启用OSR(DOTNET_TC_QuickJitForLoops=1).
但是,分层编译和OSR不仅仅是关于启动(尽管它们在那里当然非常有价值).他们还将进一步提高吞吐量.尽管分层编译最初被设想为一种在不影响吞吐量的情况下优化启动的方法,但它已经远远不止于此.JIT可以在tier-0期间了解到关于方法的各种信息,然后可以用于tier-1.例如,执行的tier-2代码意味着该方法访问的任何静态都将被初始化,这意味着任何只读静态不仅在执行tier-3代码时已经初始化,而且它们的值永远不会改变.这反过来意味着,任何原始类型的只读静态(如bool、int等)都可以被视为常量,而不是静态只读字段,并且在第1层编译期间,JIT可以优化它们,就像优化常量一样.例如,在将DOTNET_JitDisasm设置为Program:Test后,尝试运行以下简单程序:
using System.Runtime.CompilerServices;
class Program
{
static readonly bool Is64Bit = Environment.Is64BitProcess;
static int Main()
{
int count = 0;
for (int i = 0; i < 1_000_000_000; i++)
if (Test())
count++;
return count;
}
[MethodImpl(MethodImplOptions.NoInlining)]
static bool Test() => Is64Bit;
}
当我这样做时,我得到以下输出:
// Assembly listing for method Program:Test():bool
// Emitting BLENDED_CODE for X64 CPU with AVX - Windows
// Tier-0 compilation
// MinOpts code
// rbp based frame
// partially interruptible
G_M000_IG01: ;; offset=0000H
55 push rbp
4883EC20 sub rsp, 32
488D6C2420 lea rbp, [rsp+20H]
G_M000_IG02: ;; offset=000AH
48B9B8639A3FFC7F0000 mov rcx, 0x7FFC3F9A63B8
BA01000000 mov edx, 1
E8C220B25F call CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE
0FB60545580C00 movzx rax, byte ptr [(reloc 0x7ffc3f9a63ea)]
G_M000_IG03: ;; offset=0025H
4883C420 add rsp, 32
5D pop rbp
C3 ret
// Total bytes of code 43
// Assembly listing for method Program:Test():bool
// 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
G_M000_IG02: ;; offset=0000H
B801000000 mov eax, 1
G_M000_IG03: ;; offset=0005H
C3 ret
// Total bytes of code 6
注意,我们再次看到程序的两个输出:Test方法的汇编代码.首先,我们看到“Tier-0”代码,它正在访问静态(注意调用CORINFO_HELP_GETSHARED_NONGCSTATIC_BASE指令).但是,我们看到了“Tier-1”代码,其中所有的开销都消失了,取而代之的是mov eax,1.由于必须执行“Tier-0”代码才能分层,“Tier-2”代码是在知道静态只读bool IS64位字段的值为真的情况下生成的(1),因此,整个方法是将值1存储到用于返回值的eax寄存器中.
这非常有用,现在编写组件时都考虑到了分层.考虑一下新的Regex源代码生成器,这将在后面的文章中讨论(Roslyn源代码生成器是几年前引入的;就像Roslyn分析器能够插入编译器并基于编译器从源代码中学习到的所有数据提供额外的诊断一样,Roslyn源代码生成器能够分析相同的数据,然后用额外的源代码进一步增加编译单元).Regex源生成器应用基于此的dotnet/runtime#67775技术.Regex支持设置流程范围的超时,该超时将应用于没有显式设置超时的Regex实例.这意味着,即使设置这样一个进程范围的超时非常罕见,Regex源生成器仍然需要输出与超时相关的代码,以备需要.它通过输出一些helper来实现,像这样:
static class Utilities
{
internal static readonly TimeSpan s_defaultTimeout = AppContext.GetData("REGEX_DEFAULT_MATCH_TIMEOUT") is TimeSpan timeout ? timeout : Timeout.InfiniteTimeSpan;
internal static readonly bool s_hasTimeout = s_defaultTimeout != Timeout.InfiniteTimeSpan;
}
然后调用的方式,如下所示:
if (Utilities.s_hasTimeout)
{
base.CheckTimeout();
}
在第0层中,这些检查仍然会在程序集代码中发出,但在吞吐量很重要的第1层中,如果没有设置相关的AppContext开关,那么s_defaultTimeout将会是Timeout.infinittimeespan,此时s_hasTimeout将为false.由于s_hasTimeout是一个静态的只读bool, JIT将能够将其视为const,并且所有的条件,如if (Utilities.s_hasTimeout)将被视为与if (false)相等,并从汇编代码中完全清除为死代码.
但是,这有点老生常谈了.自从.NET Core 3.0引入分层编译以来,JIT就能够进行这样的优化.不过,现在在.NET 7中,在OSR中,它也可以在默认情况下对带有循环的方法进行优化(因此启用了类似于正则表达式的情况).然而,当OSR与另一个令人兴奋的特性:动态PGO相结合时,OSR的真正魔力开始发挥作用.
因JIT部分内容太多,这里进行拆分,PGO拆分为一篇博文.
个人能力有限,如果您发现有什么不对,请私信我
如果您觉得对您有用的话,可以点个赞或者加个关注,欢迎大家一起进行技术交流
相关推荐
- 快递查询教程,批量查询物流,一键管理快递
-
作为商家,每天需要查询许许多多的快递单号,面对不同的快递公司,有没有简单一点的物流查询方法呢?小编的回答当然是有的,下面随小编一起来试试这个新技巧。需要哪些工具?安装一个快递批量查询高手快递单号怎么快...
- 一键自动查询所有快递的物流信息 支持圆通、韵达等多家快递
-
对于各位商家来说拥有一个好的快递软件,能够有效的提高自己的工作效率,在管理快递单号的时候都需要对单号进行表格整理,那怎么样能够快速的查询所有单号信息,并自动生成表格呢?1、其实方法很简单,我们不需要一...
- 快递查询单号查询,怎么查物流到哪了
-
输入单号怎么查快递到哪里去了呢?今天小编给大家分享一个新的技巧,它支持多家快递,一次能查询多个单号物流,还可对查询到的物流进行分析、筛选以及导出,下面一起来试试。需要哪些工具?安装一个快递批量查询高手...
- 3分钟查询物流,教你一键批量查询全部物流信息
-
很多朋友在问,如何在短时间内把单号的物流信息查询出来,查询完成后筛选已签收件、筛选未签收件,今天小编就分享一款物流查询神器,感兴趣的朋友接着往下看。第一步,运行【快递批量查询高手】在主界面中点击【添...
- 快递单号查询,一次性查询全部物流信息
-
现在各种快递的查询方式,各有各的好,各有各的劣,总的来说,还是有比较方便的。今天小编就给大家分享一个新的技巧,支持多家快递,一次能查询多个单号的物流,还能对查询到的物流进行分析、筛选以及导出,下面一起...
- 快递查询工具,批量查询多个快递快递单号的物流状态、签收时间
-
最近有朋友在问,怎么快速查询单号的物流信息呢?除了官网,还有没有更简单的方法呢?小编的回答当然是有的,下面一起来看看。需要哪些工具?安装一个快递批量查询高手多个京东的快递单号怎么快速查询?进入快递批量...
- 快递查询软件,自动识别查询快递单号查询方法
-
当你拥有多个快递单号的时候,该如何快速查询物流信息?比如单号没有快递公司时,又该如何自动识别再去查询呢?不知道如何操作的宝贝们,下面随小编一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号若干...
- 教你怎样查询快递查询单号并保存物流信息
-
商家发货,快递揽收后,一般会直接手动复制到官网上一个个查询物流,那么久而久之,就会觉得查询变得特别繁琐,今天小编给大家分享一个新的技巧,下面一起来试试。教程之前,我们来预览一下用快递批量查询高手...
- 简单几步骤查询所有快递物流信息
-
在高峰期订单量大的时候,可能需要一双手当十双手去查询快递物流,但是由于逐一去查询,效率极低,追踪困难。那么今天小编给大家分享一个新的技巧,一次能查询多个快递单号的物流,下面一起来学习一下,希望能给大家...
- 物流单号查询,如何查询快递信息,按最后更新时间搜索需要的单号
-
最近有很多朋友在问,如何通过快递单号查询物流信息,并按最后更新时间搜索出需要的单号呢?下面随小编一起来试试吧。需要哪些工具?安装一个快递批量查询高手快递单号若干怎么快速查询?运行【快递批量查询高手】...
- 连续保存新单号功能解析,导入单号查询并自动识别批量查快递信息
-
快递查询已经成为我们日常生活中不可或缺的一部分。然而,面对海量的快递单号,如何高效、准确地查询每一个快递的物流信息,成为了许多人头疼的问题。幸运的是,随着科技的进步,一款名为“快递批量查询高手”的软件...
- 快递查询教程,快递单号查询,筛选更新量为1的单号
-
最近有很多朋友在问,怎么快速查询快递单号的物流,并筛选出更新量为1的单号呢?今天小编给大家分享一个新方法,一起来试试吧。需要哪些工具?安装一个快递批量查询高手多个快递单号怎么快速查询?运行【快递批量查...
- 掌握批量查询快递动态的技巧,一键查找无信息记录的两种方法解析
-
在快节奏的商业环境中,高效的物流查询是确保业务顺畅运行的关键。作为快递查询达人,我深知时间的宝贵,因此,今天我将向大家介绍一款强大的工具——快递批量查询高手软件。这款软件能够帮助你批量查询快递动态,一...
- 从复杂到简单的单号查询,一键清除单号中的符号并批量查快递信息
-
在繁忙的商务与日常生活中,快递查询已成为不可或缺的一环。然而,面对海量的单号,逐一查询不仅耗时费力,还容易出错。现在,有了快递批量查询高手软件,一切变得简单明了。只需一键,即可搞定单号查询,一键处理单...
- 物流单号查询,在哪里查询快递
-
如果在快递单号多的情况,你还在一个个复制粘贴到官网上手动查询,是一件非常麻烦的事情。于是乎今天小编给大家分享一个新的技巧,下面一起来试试。需要哪些工具?安装一个快递批量查询高手快递单号怎么快速查询?...
你 发表评论:
欢迎- 一周热门
- 最近发表
- 标签列表
-
- wireshark怎么抓包 (75)
- qt sleep (64)
- cs1.6指令代码大全 (55)
- factory-method (60)
- sqlite3_bind_blob (52)
- hibernate update (63)
- c++ base64 (70)
- nc 命令 (52)
- wm_close (51)
- epollin (51)
- sqlca.sqlcode (57)
- lua ipairs (60)
- tv_usec (64)
- 命令行进入文件夹 (53)
- postgresql array (57)
- statfs函数 (57)
- .project文件 (54)
- lua require (56)
- for_each (67)
- c#工厂模式 (57)
- wxsqlite3 (66)
- dmesg -c (58)
- fopen参数 (53)
- tar -zxvf -c (55)
- 速递查询 (52)