LOGO OA教程 ERP教程 模切知識交流 PMS教程 CRM教程 開發(fā)文檔 其他文檔  
 
網(wǎng)站管理員

.NET 的全新低延時高吞吐自適應(yīng) GC - Satori GC

freeflydom
2025年6月3日 10:40 本文熱度 142

GC 的 STW 問題#

GC,垃圾回收器,本質(zhì)上是一種能夠自動管理自己分配的內(nèi)存的生命周期的內(nèi)存分配器。這種方法被大多數(shù)流行編程語言采用,然而當(dāng)你使用垃圾回收器時,你會失去對應(yīng)用程序如何管理內(nèi)存的控制。C# 允許在自動控制內(nèi)存的基礎(chǔ)之上局部對內(nèi)存進行手動控制,但是自動控制仍然是主要的場景。

然而 GC 總是需要暫停程序的運行以遍歷和識別存活的對象,從而刪除無效對象以及進行維護操作(例如通過移動對象到更緊湊的內(nèi)存區(qū)域以減少內(nèi)存碎片,這個過程也叫做壓縮)。GC 暫停整個程序的行為也叫做 STW(Stop-The-World)。這個暫停時間越長,對應(yīng)用的影響越大。

長期以來,.NET 的 GC 都一直在朝著優(yōu)化吞吐量性能和內(nèi)存占用的方向不斷優(yōu)化,這對于 Web 應(yīng)用以及跑在容器中的服務(wù)而言非常適合。而在客戶端、游戲和金融領(lǐng)域,開發(fā)人員一直都需要格外注意代碼中的分配問題,例如使用對象池、值類型以及非托管內(nèi)存等等,避免產(chǎn)生大量的垃圾和各種 GC 難以處理的反模式,以此來減少 GC 的單次暫停時間。例如在游戲中,要做到 60fps,留給每一幀的時間只有 16ms,這其中如果 GC 單次暫停時間過長,用戶就會觀察到明顯的掉幀。

Workstation GC?Server GC?DATAS GC?#

.NET 一直以來都有兩種 GC 模式 —— Workstation GC 和 Server GC。

Workstation GC 是 .NET 最古老的 GC 模式,其目標(biāo)之一是最小化內(nèi)存占用,以適配資源有限的場景。在 Workstation GC 中,它只會利用你一個 CPU 核心,因此哪怕你有多核的計算資源,Workstation GC 也不會去使用它們來優(yōu)化分配性能,雖然 Workstation GC 同樣支持后臺回收,但即使開啟后臺回收,Workstation GC 也之多只會用一個后臺線程。這么一來其性能發(fā)揮就會受到不小的限制。面對大量分配和大量回收場景時 Workstation GC 則顯得力不從心。不過,當(dāng)你的應(yīng)用很輕量并且不怎么分配內(nèi)存的時候,Workstation GC 將是一個很適合的選擇。

而之后誕生的 Server GC 則可以有效的利用多核計算資源,根據(jù) CPU 核心數(shù)量來控制托管堆數(shù)量,大幅度提升了吞吐量。然而 Server GC 的缺點也很明顯——內(nèi)存占用大。另外,Server GC 雖然通過并發(fā) GC 等方式將一部分工作移動到 STW 之外,從而使得 GC 和應(yīng)用程序可以同時運行,讓 STW 得到了不小的改進,然而 Server GC 的暫停時間仍然稱不上優(yōu)秀,雖然在 Web 服務(wù)等應(yīng)用場景下表現(xiàn)得不錯,然而在一些極端情況下則可能需要暫停上百毫秒。

為了進一步改善 Server GC 的綜合表現(xiàn),.NET 9 引入了新的 DATAS GC,試圖在優(yōu)化內(nèi)存占用的同時提升暫停時間表現(xiàn)。這個 GC 通過引入各種啟發(fā)算法自適應(yīng)應(yīng)用場景來最小化內(nèi)存占用的同時,也改善了暫停時間。測試表明 DATAS GC 相比 Server GC 雖然犧牲了個位數(shù)百分比的吞吐量性能,卻成功的減少了 70%~90% 的內(nèi)存占用的同時,暫停時間也縮減到 Server GC 的 1/3。

然而,這仍然不能算是完美的解決方案。開發(fā)者們都是抱著既要又要還要的心理,需要的是一個既能做到大吞吐量,暫停時間又短,同時內(nèi)存占用還小的 GC。

因此,.NET 全新的 GC —— 在 .NET Runtime 核心成員幾年的努力下誕生了!這就是接下來我要講的 Satori GC。

Satori GC

為了讓 GC 能夠正確追蹤對象,在不少語言中,編譯器會給存儲操作插入一個寫屏障。在寫屏障中 GC 會更新對象的引用從而確保每一個對象都能夠正確被追蹤。這么做的好處很明顯,相比讀操作而言,寫操作更少,將屏障分擔(dān)到每次的寫操作里顯然是一個更有效率的方法。然而這么做的壞處也很明顯:當(dāng) GC 需要執(zhí)行壓縮操作時不得不暫停整個程序,避免代碼訪問到無效的內(nèi)存地址。

而 JVM 上的一些低延時 GC 則放棄了寫屏障,轉(zhuǎn)而使用讀屏障,在每次讀取內(nèi)存地址的時候通過插入屏障來確保始終拿到的是最新的內(nèi)存地址,來避免無效地址訪問。然而讀操作在應(yīng)用中非常頻繁,這么做雖然能夠使得 GC 執(zhí)行壓縮操作時不再需要暫停整個程序,卻會不可避免地帶來性能的損失。

GC 執(zhí)行壓縮操作雖然開銷很大,但相對于釋放操作而言只是少數(shù),為了少數(shù)的操作能夠并發(fā)執(zhí)行拖慢所有的讀操作顯得有些得不償失。另外,在 .NET 上,由于 .NET 支持內(nèi)部指針和固定對象的內(nèi)存地址,因此讀屏障在 .NET 上實現(xiàn)較為困難,并且會帶來吞吐量的嚴(yán)重下降,在許多場景下難以接受。

.NET 的新低延時高吞吐自適應(yīng) GC —— Satori GC 仍然采用 Dijkstra 風(fēng)格的寫屏障設(shè)計,因此吞吐量性能仍然能夠匹敵已有的 Server GC。

另外,Satori GC 采用了分代、增量并發(fā)回收設(shè)計,所有與堆大小成比例的主要 GC 階段都會與應(yīng)用程序線程并發(fā)執(zhí)行,完全不需要暫停應(yīng)用程序,除了壓縮過程之外。不過,壓縮僅僅是 GC 可以執(zhí)行但不是必須執(zhí)行的一個可選項。例如 C++/Rust 的內(nèi)存分配器也不會進行壓縮,但仍能正常運行;Go 的 GC 也不會進行壓縮。

除了標(biāo)準(zhǔn)模式之外,Satori GC 還提供了低延時模式。在這個模式下 Satori GC 直接關(guān)閉了壓縮功能,通過犧牲少量的內(nèi)存占用來獲取更低的延時。在某些情況下,因為垃圾回收發(fā)生得更快,或者壓縮本身并沒有帶來太多實際收益,內(nèi)存占用反而會變得更小。例如在一些 Web 場景,大量對象只存活在單次請求期間,然后很快就會被清除。既然這些對象很快都會變成垃圾,那為什么要進行壓縮呢?

與 Go 的徹底不進行壓縮的 GC 不同,Satori GC 可以在運行時動態(tài)切換壓縮的開關(guān)狀態(tài),以適應(yīng)不同的應(yīng)用場景。想要開啟低延時模式,只需要執(zhí)行 GCSettings.LatencyMode = GCLatencyMode.LowLatency 即可。在需要極低延時的場景(例如高幀率游戲或金融實時交易系統(tǒng))中,這一設(shè)置可以有效減少 GC 暫停時間。

Satori GC 還允許開發(fā)者根據(jù)需要關(guān)閉 Gen 0:畢竟不是所有的場景/應(yīng)用都能從 Gen 0 中獲益。當(dāng)應(yīng)用程序并不怎么用到 Gen 0 時,為了支持 Gen 0 在寫屏障中做的額外操作反而會拖慢性能。目前可以通過設(shè)置環(huán)境變量 DOTNET_gcGen0=0 來關(guān)閉 Gen 0,不過在 Satori GC 計劃中,將會實現(xiàn)根據(jù)實際應(yīng)用場景自動決策 Gen 0 的開啟與關(guān)閉。

性能測試

說了這么多,新的 Satori GC 到底療效如何呢?讓我們擺出來性能測試來看看。

首先要說的是,測試前需要設(shè)置 <TieredCompilation>false</TieredCompilation> 關(guān)閉分層編譯,因為 tier-0 的未優(yōu)化代碼會影響對象的生命周期,從而影響 GC 行為。

測試場景 1

Unity 有一個 GC 壓力測試,游戲在每次更新都需要渲染出一幀畫面,而這個測試則模擬了游戲在每幀中分配大量的數(shù)據(jù),但是卻不渲染任何的內(nèi)容,從而通過單幀時間來反映 GC 的實際暫停。

代碼如下:

Copy
class Program {    const int kLinkedListSize = 1000;    const int kNumLinkedLists = 10000;    const int kNumLinkedListsToChangeEachFrame = 10;    private const int kNumFrames = 100000;    private static Random r = new Random();    class ReferenceContainer    {        public ReferenceContainer rf;    }    static ReferenceContainer MakeLinkedList()    {        ReferenceContainer rf = null;        for (int i = 0; i < kLinkedListSize; i++)        {            ReferenceContainer link = new ReferenceContainer();            link.rf = rf;            rf = link;        }        return rf;    }    static ReferenceContainer[] refs = new ReferenceContainer[kNumLinkedLists];    static void UpdateLinkedLists(int numUpdated)    {        for (int i = 0; i < numUpdated; i++)        {            refs[r.Next(kNumLinkedLists)] = MakeLinkedList();        }    }    static void Main(string[] args)    {        GCSettings.LatencyMode = GCLatencyMode.LowLatency;                float maxMs = 0;        UpdateLinkedLists(kNumLinkedLists);        Stopwatch totalStopWatch = new Stopwatch();        Stopwatch frameStopWatch = new Stopwatch();        totalStopWatch.Start();        for (int i = 0; i < kNumFrames; i++)        {            frameStopWatch.Start();            UpdateLinkedLists(kNumLinkedListsToChangeEachFrame);            frameStopWatch.Stop();            if (frameStopWatch.ElapsedMilliseconds > maxMs)                maxMs = frameStopWatch.ElapsedMilliseconds;            frameStopWatch.Reset();        }        totalStopWatch.Stop();                Console.WriteLine($"Max Frame: {maxMs}, Avg Frame: {(float)totalStopWatch.ElapsedMilliseconds/kNumFrames}");    } }

測試結(jié)果如下:

GC最大幀時間平均幀時間峰值內(nèi)存占用
Server GC323 ms0.049ms5071.906 MB
DATAS GC139 ms0.146ms1959.301 MB
Workstation GC23 ms0.563 ms563.363 MB
Satori GC26 ms0.061 ms1449.582 MB
Satori GC (低延時)8 ms0.050 ms1540.891 MB
Satori GC (低延時,關(guān) Gen 0)3 ms0.042 ms1566.848 MB

可以看到 Satori GC 在擁有 Server GC 的吞吐量性能同時(平均幀時間),還擁有著優(yōu)秀的最大暫停時間(最大幀時間)。

測試場景 2

在這個測試中,代碼中將產(chǎn)生大量的 Gen 2 -> Gen 0 的反向引用讓 GC 變得非常繁忙,然后通過大量分配生命周期很短的臨時對象觸發(fā)大量的 Gen 0 GC。

Copy
using System.Diagnostics; using System.Runtime; using System.Runtime.CompilerServices; object[] a = new object[100_000_000]; var sw = Stopwatch.StartNew(); var sw2 = Stopwatch.StartNew(); var count = GC.CollectionCount(0) + GC.CollectionCount(1) + GC.CollectionCount(2); for (var iter = 0; ; iter++) {    // Create a lot of Gen2 -> Gen0 references to keep the GC busy    object o = new object();    for (int i = 0; i < a.Length; i++)    {        a[i] = o;    }    sw.Restart();    // Use the object to keep it alive    Use(a, o);    // Create a lot of short lived objects to trigger Gen0 GC    for (int i = 0; i < 1000; i++)    {        GC.KeepAlive(new string('a', 10000));    }    var newCount = GC.CollectionCount(0) + GC.CollectionCount(1) + GC.CollectionCount(2);    if (newCount != count)    {        Console.WriteLine($"Gen0: {GC.CollectionCount(0)}, Gen1: {GC.CollectionCount(1)}, Gen2: {GC.CollectionCount(2)}, Pause on Gen0: {sw.ElapsedMilliseconds}ms, Throughput: {(iter + 1) / sw2.Elapsed.TotalSeconds} iters/sec, Max Working Set: {Process.GetCurrentProcess().PeakWorkingSet64 / 1048576.0} MB");        count = newCount;        iter = -1;        sw2.Restart();    } } [MethodImpl(MethodImplOptions.NoInlining)] static void Use(object[] arr, object obj) { }

由于這個測試主要就是測試 Gen 0 的回收性能,因此測試結(jié)果中將不包含關(guān)閉 Gen 0 的情況。

GC單次暫停吞吐量峰值內(nèi)存占用
Server GC59 ms7.485 iter/s1286.898 MB
DATAS GC60 ms6.362 iter/s859.722 MB
Workstation GC1081 ms0.804 iter/s805.453 MB
Satori GC0 ms4.448 iter/s801.441 MB
Satori GC (低延時)0 ms4.480 iter/s804.761 MB

這個測試中 Satori GC 表現(xiàn)得非常亮眼:擁有不錯的吞吐量性能的同時,做到了亞毫秒級別的暫停時間:可以說在這個測試中 Satori GC 壓根就沒有暫停過我們的應(yīng)用程序!

測試場景 3

這次我們使用 BinaryTree Benchmark 進行測試,這個測試由于會短時間大量分配對象,因此對于 GC 而言是一項壓力很大的測試。

Copy
using System.Diagnostics; using System.Diagnostics.Tracing; using System.Runtime; using System.Runtime.CompilerServices; using Microsoft.Diagnostics.NETCore.Client; using Microsoft.Diagnostics.Tracing; using Microsoft.Diagnostics.Tracing.Analysis; using Microsoft.Diagnostics.Tracing.Parsers; class Program {    [MethodImpl(MethodImplOptions.AggressiveOptimization)]    static void Main()    {        var pauses = new List<double>();        var client = new DiagnosticsClient(Environment.ProcessId);        EventPipeSession eventPipeSession = client.StartEventPipeSession([new("Microsoft-Windows-DotNETRuntime",            EventLevel.Informational, (long)ClrTraceEventParser.Keywords.GC)], false);        var source = new EventPipeEventSource(eventPipeSession.EventStream);        source.NeedLoadedDotNetRuntimes();        source.AddCallbackOnProcessStart(proc =>        {            proc.AddCallbackOnDotNetRuntimeLoad(runtime =>            {                runtime.GCEnd += (p, gc) =>                {                    if (p.ProcessID == Environment.ProcessId)                    {                        pauses.Add(gc.PauseDurationMSec);                    }                };            });        });        GC.Collect(GC.MaxGeneration, GCCollectionMode.Aggressive, true, true);        GC.WaitForPendingFinalizers();        GC.WaitForFullGCComplete();        Thread.Sleep(5000);        new Thread(() => source.Process()).Start();        pauses.Clear();        Test(22);                source.StopProcessing();        Console.WriteLine($"Max GC Pause: {pauses.Max()}ms");        Console.WriteLine($"Average GC Pause: {pauses.Average()}ms");        pauses.Sort();        Console.WriteLine($"P99.9 GC Pause: {pauses.Take((int)(pauses.Count * 0.999)).Max()}ms");        Console.WriteLine($"P99 GC Pause: {pauses.Take((int)(pauses.Count * 0.99)).Max()}ms");        Console.WriteLine($"P95 GC Pause: {pauses.Take((int)(pauses.Count * 0.95)).Max()}ms");        Console.WriteLine($"P90 GC Pause: {pauses.Take((int)(pauses.Count * 0.9)).Max()}ms");        Console.WriteLine($"P80 GC Pause: {pauses.Take((int)(pauses.Count * 0.8)).Max()}ms");        using (var process = Process.GetCurrentProcess())        {            Console.WriteLine($"Peak WorkingSet: {process.PeakWorkingSet64} bytes");        }    }    static void Test(int size)    {        var bt = new BinaryTrees.Benchmarks();        var sw = Stopwatch.StartNew();        bt.ClassBinaryTree(size);        Console.WriteLine($"Elapsed: {sw.Elapsed.TotalMilliseconds}ms");    } } public class BinaryTrees {    class ClassTreeNode    {        class Next { public required ClassTreeNode left, right; }        readonly Next? next;        ClassTreeNode(ClassTreeNode left, ClassTreeNode right) =>            next = new Next { left = left, right = right };        public ClassTreeNode() { }        internal static ClassTreeNode Create(int d)        {            return d == 1 ? new ClassTreeNode(new ClassTreeNode(), new ClassTreeNode())                          : new ClassTreeNode(Create(d - 1), Create(d - 1));        }        internal int Check()        {            int c = 1;            var current = next;            while (current != null)            {                c += current.right.Check() + 1;                current = current.left.next;            }            return c;        }    }    public class Benchmarks    {        const int MinDepth = 4;        public int ClassBinaryTree(int maxDepth)        {            var longLivedTree = ClassTreeNode.Create(maxDepth);            var nResults = (maxDepth - MinDepth) / 2 + 1;            for (int i = 0; i < nResults; i++)            {                var depth = i * 2 + MinDepth;                var n = 1 << maxDepth - depth + MinDepth;                var check = 0;                for (int j = 0; j < n; j++)                {                    check += ClassTreeNode.Create(depth).Check();                }            }            return longLivedTree.Check();        }    } }

這一次我們使用 Microsoft.Diagnostics.NETCore.Client 來精準(zhǔn)的跟蹤每一次 GC 的暫停時間。

測試結(jié)果如下:

性能指標(biāo)Workstation GCServer GCDATAS GCSatori GCSatori GC (低延時)Satori GC (關(guān) Gen 0)
執(zhí)行所要時間 (ms)63,611.395422,645.352524,881.611441,515.633340,642.300813528.3383
峰值內(nèi)存占用 (bytes)1,442,217,9844,314,828,8002,076,291,0721,734,955,0081,537,855,4881,541,136,384
最大暫停時間 (ms)48.9107259.9675197.72126.52394.09791.2347
平均暫停時間 (ms)6.11728238312.007850673.3040141640.6734356910.4377585530.1391
P99.9 暫停時間 (ms)46.8537243.2844172.32595.85353.68350.9887
P99 暫停時間 (ms)44.0532207.362757.46815.26613.20120.5814
P95 暫停時間 (ms)39.490348.72698.923.00541.38540.3536
P90 暫停時間 (ms)23.132721.45882.80131.78590.92040.2681
P80 暫停時間 (ms)8.33174.75771.75810.80090.60060.1942

這一次 Satori GC 的標(biāo)準(zhǔn)模式和低延時模式都做到了非常低的延時,而關(guān)閉 Gen 0 后 Satori GC 更是直接衛(wèi)冕 GC 之王,不僅執(zhí)行性能上跑過了 Server GC,同時還做到了接近 Workstation GC 級別的內(nèi)存占用,并且還做到了亞毫秒級別的最大 STW 時間!

測試場景 4

這個場景來自社區(qū)貢獻的 GC 測試:https://github.com/alexyakunin/GCBurn

這個測試包含三個不同的重分配的測試項目,模擬三種場景:

  • Cache Server:使用一半的內(nèi)存(大約 16G),分配大約 1.86 億個對象
  • Stateless Server:一個無狀態(tài) Web Server
  • Worker Server:一個有狀態(tài)的 Worker 始終占據(jù) 20% 內(nèi)存(大約 6G),分配大概 7400 萬個對象

測試結(jié)果由其他社區(qū)成員提供。

首先看分配速率:

Server GC 是針對吞吐量進行大量優(yōu)化的,因此做到最高的吞吐量性能并不意外。Satori GC 在 Cache Server 場景有所落后,但是現(xiàn)實中并不會有在一秒內(nèi)分配超過 2 千萬個對象的場景,因此這個性能水平并不會造成實際的性能瓶頸。

然后看暫停時間:

注意時間的單位是微秒(0.001ms),并且縱坐標(biāo)進行了對數(shù)縮放。Satori GC 成功地做到了亞毫秒(小于 1000 微秒)級別的暫停。

最后看峰值內(nèi)存占用:

可以看到 Satori GC 相比其他 GC 而言有著出色的內(nèi)存占用,在所有測試結(jié)果中幾乎都是那個內(nèi)存占用最低的。

綜合以上三點,我們可以看到 Satori GC 在犧牲少量的吞吐量性能的同時,做到了亞毫秒級別的延時和低內(nèi)存占用。只能說:干得漂亮!

大量分配速率測試

這同樣是來自其他社區(qū)成員貢獻的測試結(jié)果。在這個測試中,代碼使用一個循環(huán)在所有的線程上大量分配對象并立馬釋放。

測試結(jié)果如下:

可以看到 Satori GC 的默認(rèn)模式在這個測試中做到了最好的分配吞吐量性能,成功做到每秒分配 20 億個對象。

總結(jié)

Satori GC 的目標(biāo)是為 .NET 帶來了一種全新的低延時高吞吐自適應(yīng) GC,不僅有著優(yōu)秀的分配速率,同時還能做到亞毫秒級別的暫停時間和低內(nèi)存占用,與此同時做到 0 配置開箱即用。

目前 Satori GC 仍然處于實驗性階段,還有不少的課題需要解決,例如運行時自動決策 Gen 0 的開關(guān)、更好的策略以均衡吞吐量性能和內(nèi)存占用以及適配更新版本的 .NET 等等,但是已經(jīng)可以用于真實世界應(yīng)用了,想要試用 Satori GC 的話可以參考下面的方法在自己的應(yīng)用中啟用。

osu! 作為一款從引擎到游戲客戶端都是純 C# 開發(fā)的游戲,已經(jīng)開始提供使用 Satori GC 的選項。在選歌列表的滾動測試中,Satori GC 成功將幀數(shù)翻了一倍,從現(xiàn)在的 120 fps 左右提升到接近 300 fps。

相信等 Satori GC 成熟正式作為 .NET 默認(rèn) GC 啟用后,將會為 .NET 帶來大量的性能提升,并擴展到更多的應(yīng)用場景。

啟用方法

截至目前(2025/5/22),Satori GC 僅支持在 .NET 8 應(yīng)用中啟用??紤]到這是目前最新的 LTS 穩(wěn)定版本,因此目前來看也足夠了,另外 Satori GC 的開發(fā)人員已經(jīng)在著手適配 .NET 9。

osu! 團隊配置了 CI 自動構(gòu)建最新的 Satori GC,因此我們不需要手動構(gòu)建 .NET Runtime 源碼得到 GC,直接下載對應(yīng)的二進制即可使用。

對于 .NET 8 應(yīng)用:

  1. 使用 dotnet publish -c Release -r <rid> --self-contained 發(fā)布一個自包含應(yīng)用,例如 dotnet publish -c Release -r win-x64 --self-contained
  2. 從 https://github.com/ppy/Satori/releases 下載對應(yīng)平臺的最新 Satori GC 構(gòu)建,例如 win-x64.zip
  3. 解壓得到三個文件:如果是 Windows 平臺就是 coreclr.dll、clrjit.dll 和 System.Private.CoreLib.dll;而如果是 Linux 則是 libcoreclr.so、libclrjit.so 和 System.Private.CoreLib.dll
  4. 找到第一步發(fā)布出來的應(yīng)用(一般在 bin/Release/net8.0/<rid>/publish 文件夾里,例如 bin/Release/net8.0/win-x64/publish),用第三步得到的三個文件替換掉發(fā)布目錄里面的同名文件

然后即可享受 Satori GC 帶來的低延時體驗。

反饋渠道

如果在使用過程中遇到了任何問題,可以參考:

轉(zhuǎn)自https://www.cnblogs.com/hez2010/p/18889954/


該文章在 2025/6/3 10:41:46 編輯過
關(guān)鍵字查詢
相關(guān)文章
正在查詢...
點晴ERP是一款針對中小制造業(yè)的專業(yè)生產(chǎn)管理軟件系統(tǒng),系統(tǒng)成熟度和易用性得到了國內(nèi)大量中小企業(yè)的青睞。
點晴PMS碼頭管理系統(tǒng)主要針對港口碼頭集裝箱與散貨日常運作、調(diào)度、堆場、車隊、財務(wù)費用、相關(guān)報表等業(yè)務(wù)管理,結(jié)合碼頭的業(yè)務(wù)特點,圍繞調(diào)度、堆場作業(yè)而開發(fā)的。集技術(shù)的先進性、管理的有效性于一體,是物流碼頭及其他港口類企業(yè)的高效ERP管理信息系統(tǒng)。
點晴WMS倉儲管理系統(tǒng)提供了貨物產(chǎn)品管理,銷售管理,采購管理,倉儲管理,倉庫管理,保質(zhì)期管理,貨位管理,庫位管理,生產(chǎn)管理,WMS管理系統(tǒng),標(biāo)簽打印,條形碼,二維碼管理,批號管理軟件。
點晴免費OA是一款軟件和通用服務(wù)都免費,不限功能、不限時間、不限用戶的免費OA協(xié)同辦公管理系統(tǒng)。
Copyright 2010-2025 ClickSun All Rights Reserved

黄频国产免费高清视频,久久不卡精品中文字幕一区,激情五月天AV电影在线观看,欧美国产韩国日本一区二区
五月天综合久久久 | 亚洲欧美日韩动漫一区二区 | 伊人大杳焦在久久综合网 | 亚洲日韩人人爽 | 在线观看非常黄的永久网站 | 一区二图三区国产精品 |