KumoKyaku / kcp

KCP C#版。线程安全,运行时无alloc,对gc无压力。
MIT License
832 stars 136 forks source link

在收发消息的时候,segment频繁地在非托管堆申请和释放内存,导致频繁地在用户态和内核态切换,占用很高的CPU #12

Closed paxhujing closed 2 years ago

paxhujing commented 3 years ago

1000个连接,CPU占用50%以上,内核调用占48%

AsukaLay commented 3 years ago

扣一块内存下来用嘛

KumoKyaku commented 3 years ago

没什么写内存管理的经验……力不从心啊

KumoKyaku commented 3 years ago

7c1ab237d5218dd246b423fb4696037d1a1bf5a2

公开了KcpSegment,现在大家可以自定义了,暂时先用这种方式回避性能问题,有时间我看看能不能再写一个更高效实现。

yourFinger commented 3 years ago

KcpSegment在收发的时候new, 导致gc频繁回收,建议设计一个KcpSegmentPool去缓存KcpSegment

KumoKyaku commented 3 years ago

KcpSegment在收发的时候new, 导致gc频繁回收,建议设计一个KcpSegmentPool去缓存KcpSegment

KcpSegment 是动态长度的,pool比较难实现

RonTang commented 3 years ago

我在Mirror开源项目看到其实现利用了MemoryStream,或许你能从这里找到点灵感,另外微软还开源了一个更高效MemoryStream实现

using System.Collections.Generic;
using System.IO;

namespace kcp2k
{
    // KCP Segment Definition
    internal class Segment
    {
        internal uint conv;     // conversation
        internal uint cmd;      // command, e.g. Kcp.CMD_ACK etc.
        internal uint frg;      // fragment
        internal uint wnd;      // window size that the receive can currently receive
        internal uint ts;       // timestamp
        internal uint sn;       // serial number
        internal uint una;
        internal uint resendts; // resend timestamp
        internal int rto;
        internal uint fastack;
        internal uint xmit;
        // we need a auto scaling byte[] with a WriteBytes function.
        // MemoryStream does that perfectly, no need to reinvent the wheel.
        // note: no need to pool it, because Segment is already pooled.
        internal MemoryStream data = new MemoryStream();

        // pool ////////////////////////////////////////////////////////////////
        internal static readonly Stack<Segment> Pool = new Stack<Segment>(32);

        public static Segment Take()
        {
            if (Pool.Count > 0)
            {
                Segment seg = Pool.Pop();
                return seg;
            }
            return new Segment();
        }

        public static void Return(Segment seg)
        {
            seg.Reset();
            Pool.Push(seg);
        }
        ////////////////////////////////////////////////////////////////////////

        // ikcp_encode_seg
        // encode a segment into buffer
        internal int Encode(byte[] ptr, int offset)
        {
            int offset_ = offset;
            offset += Utils.Encode32U(ptr, offset, conv);
            offset += Utils.Encode8u(ptr, offset, (byte)cmd);
            offset += Utils.Encode8u(ptr, offset, (byte)frg);
            offset += Utils.Encode16U(ptr, offset, (ushort)wnd);
            offset += Utils.Encode32U(ptr, offset, ts);
            offset += Utils.Encode32U(ptr, offset, sn);
            offset += Utils.Encode32U(ptr, offset, una);
            offset += Utils.Encode32U(ptr, offset, (uint)data.Position);

            return offset - offset_;
        }

        // reset to return a fresh segment to the pool
        internal void Reset()
        {
            conv = 0;
            cmd = 0;
            frg = 0;
            wnd = 0;
            ts = 0;
            sn = 0;
            una = 0;
            rto = 0;
            xmit = 0;
            resendts = 0;
            fastack = 0;

            // keep buffer for next pool usage, but reset length (= bytes written)
            data.SetLength(0);
        }
    }
}
KumoKyaku commented 3 years ago

临时写了一个,有时间可以测测https://github.com/KumoKyaku/KCP/blob/62194f5223d0e98201007301c19483483181edc9/KCP/SegManager.cs#L91 @paxhujing @AsukaLay

KumoKyaku commented 3 years ago

https://github.com/KumoKyaku/KCP/blob/6b0da35aaecffd5711ba688c3d11f28a6a42f46c/KCP/SegManager.cs#L106 增加一个Segment池实现。

L-LingRen commented 3 years ago

不能把内存交给使用者自己管理么?

KumoKyaku commented 3 years ago

@L-LingRen 可以,现在用户可以定义自己的SegmentManager,库中代码中有三个例子。

paxhujing commented 3 years ago

.net core 可以使用MemoryPool