C#高性能低GC 非托管动态扩容数组

开始之前

相比固定长度的Array,大家可能在编程的时候经常会使用List<T>,同时可能会经常往里面Add东西,因为List具有可扩容性,但是注重GC的朋友会发现(比如Unity开发者),List.Resize会造成扩容前数组长度*泛型类型所占字节长度的GC,同时会造成耗时,以及额外的内存占用(比如List有100个元素的时候触发了扩容,新容量为200,但是总共一共插入了150个元素,导致有50个分配的内存没被利用)

Stream(例如MemoryStream)与List一样,在Resize里会分配当前容量两倍的新byte托管数组,也会造成和上面提到的一样的情况,导致GC和可能存在的额外内存占用,以及拷贝托管数组的耗时。

那么有没有什么办法能实现一个:

  1. 能插入元素

  2. 能动态扩容

  3. 扩容不造成GC

  4. 能指定扩容长度

包含上述内容的动态扩容数组呢?

让我们先看看List和Stream的原理

List<T>和Stream

List<T>和Stream一样,基本是内部有一个托管数组T[]byte[]

内部会记录当前总容量,以及元素总数,Stream还会额外记录当前的位置

且内部实现了Resize方法,会new一个新的托管数组,长度为当前总容量的两倍

紧接着会把老数组的元素复制到新数组上,老数组不会再被引用且造成GC

Span和Memory

最近C#提供了Span和Memory类型,提供了安全操作连续内存的方法

他们的内部实现是这样的:

  1. 记录对应泛型类型的指针

  2. 记录该指针的长度(多少个元素)

Span和Memory有一点微小的区别,比如在栈上和托管堆上(Span是ref struct,Memory则是readonly struct的缘故),导致他们的用法不太一样,不过本文只需要关心他们的实现原理。

是不是发现和List<T>以及Stream很像?只是托管数组变成指针了,然后少了一些成员?

指针

指针是什么?指针就是一个变量在内存里的地址,所以叫做指针(Pointer),因为指针指向了内存内的一个变量

在内存中的变量有两种情况,一种是被GC托管的变量,一种是不被GC托管的变量,而我们的List和Stream内部的数组,就是托管数组,由GC托管。

如果对Span和Memory熟悉的,应该知道List可以直接转Span,怎么做到的呢?只需要把List内部托管的数组的指针传给Span的构造参数就行(List转Memory也可以就是需要自己实现,有点复杂)

那么延伸的想法就来了,如果我们用非托管指针代替分配的托管数组来存我们的元素,是不是就可以不被GC托管而不被产生GC了?答案是,没错。

自行分配非托管内存

如果我们需要申请非托管内存,我们需要实现以下一条很重要原则:

  • 手动申请的非托管内存必须用好后手动释放(不然就会造成野指针)

C#有两种方法申请非托管内存,并且任何能运行C#的平台都支持(Unity也是支持的,哪怕是IL2CPP)

  1. Marshal.AllocHGlobal,该方法会返回指定长度的非托管内存,并且返回的内存有可能会有值

  2. Marshal.AllocCoTaskMem,该方法会返回至少指定长度的非托管内存,但是也有可能会返回超过改长度的内存,且返回的内存不会有值(全是0)

这里很明显,第一个提到的方法适合我们的使用场景

托管的动态扩容数组类型

既然用Sturct可以避免创建时造成的GC(如Span, Memory都是struct),为什么我们要用托管类型(Class)去定义我们的动态扩容数组呢?

请看一下上面提到的原则,手动申请的非托管内存必须用好后手动释放(不然就会造成野指针)

只有通过托管类型,我们才能做到这一点:

  • 在构造函数(Constructor内申请非托管内存)

  • 在折构函数(Finalizer内释放申请的内存)

折构函数就是一个对象被GC回收前调用的函数)

实现一个非托管类型的动态扩容数组

因为非托管类型转指针比较方便,所以本文我们先实现一个非托管类型的动态扩容数组

根据我们上面提到的思路,可以得出以下代码(注,此代码不是完整体):

/// <summary>
/// A buffer that can dynamically extend
/// </summary>
/// <typeparam name="T"></typeparam>
public sealed unsafe class ExtensibleBuffer<T> where T : unmanaged
{/// <summary>/// Init extensible buffer with a capacity/// </summary>/// <param name="size"></param>/// <param name="initialData"></param>private ExtensibleBuffer(int size, T[] initialData)
{sizeOfT = (byte)sizeof(T);ExpandSize = size;Data = (T*)Marshal.AllocHGlobal(sizeOfT * ExpandSize);if (initialData != null){fixed(T* ptr = initialData){CopyFrom(ptr, 0, 0, initialData.Length);}}TotalLength = ExpandSize;GC.AddMemoryPressure(sizeOfT * ExpandSize);
}/// <summary>/// Free allocated memories/// </summary>~ExtensibleBuffer()
{Marshal.FreeHGlobal((IntPtr)Data);GC.RemoveMemoryPressure(sizeOfT * TotalLength);
}
}

上面的代码实现了构造函数和折构函数,其中构造函数的参数指定了扩容大小,方法内部获取了泛型T的内存大小,并且申请了类型大小*扩容数量个字节的内存,并且如果有初始化数据,就把初始化托管数据复制到非托管内存上

同时,会标记目前的总长度,以及通知GC我们有申请的内存大小的内存压力(促进GC多去回收)

折构函数内,我们释放了申请的内存,同时通知GC我们之前申请的内存大小的内存压力没了,被我们释放了(让GC不要再关系我们这个动态扩容数组了)

索引器

索引器就是数组/List返回指定位置元素的方法:

/// <summary>
/// Get element at index
/// </summary>
/// <param name="index"></param>
public T this[int index]
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]get => Data[index];
[MethodImpl(MethodImplOptions.AggressiveInlining)]set
{EnsureCapacity(ref index);Data[index] = value;
}
}

我们在插入的时候检查下申请的内存就好,确保插入到有效的内存里。

实现扩容

既然要避免每次扩容都双倍现在的长度从而造成内存浪费,我们需要在构造函数里标记扩容大小,然后每次扩容的时候当前总长度+=扩容大小就好

幸运的是C#提供了一个重新分配通过Marshal.AllocHGlobal申请的内存的方法:

Marshal.ReAllocHGlobal

这个方法需要传两个参数,第一个参数是原申请的指针,第二个参数是新长度(转指针)

通过简单的封装,我们得到了:

/// <summary>
/// Ensure index exists
/// </summary>
/// <param name="index"></param>
private void EnsureCapacity(ref int index)
{if (index < TotalLength) return;while (index >= TotalLength)
{TotalLength += ExpandSize;GC.AddMemoryPressure(sizeOfT * ExpandSize);
}Extend();
}/// <summary>
/// Extend buffer
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void Extend()
{Data = (T*)Marshal.ReAllocHGlobal((IntPtr)Data, new IntPtr(TotalLength * sizeOfT));
}

我们只需要定期触发(比如每次插入的时候,访问的话为了性能我们就不检查了,因为是指针,也不会导致数组越界,只是会返回我们想不到的结果)EnsureCapacity,来检查指定的索引是否被我们申请过,如果没的话,就动态扩容以及通知GC即可

从外部的数组/指针里复制元素

我们只需要取别的数组/指针,然后从指定偏移开始,复制指定长度到我们申请的指针的指定位置即可:

/// <summary>
/// Copy data to extensible buffer
/// </summary>
/// <param name="src"></param>
/// <param name="srcIndex"></param>
/// <param name="dstIndex"></param>
/// <param name="length"></param>
/// <exception cref="InvalidOperationException"></exception>
public void CopyFrom(T[] src, int srcIndex, int dstIndex, int length)
{fixed (T* ptr = src)
{CopyFrom(ptr, srcIndex, dstIndex, length);
}
}/// <summary>
/// Copy data to extensible buffer
/// why unaligned? https://stackoverflow.com/a/72418388
/// </summary>
/// <param name="src"></param>
/// <param name="srcIndex"></param>
/// <param name="dstIndex"></param>
/// <param name="length"></param>
/// <exception cref="InvalidOperationException"></exception>
public void CopyFrom(T* src, int srcIndex, int dstIndex, int length)
{var l = dstIndex + length;//size checkEnsureCapacity(ref l);//copyUnsafe.CopyBlockUnaligned(Data + dstIndex, src + srcIndex, (uint)length);
}

StackOverFlow的这篇文章:stackoverflow.com/a/724证明了不对齐的拷贝内存更快,不过这里我们是非托管类型的非托管内存,所以这样玩不会出问题

复制数据到外部的数组/指针

与上面的实现类似,我们只需要获取需要复制到的数组/指针,从我们动态扩容数组的第几个元素开始复制,复制多少个即可

注,这里如果需要复制到指定的数组位置,可以把数组转指针后+偏移,然后调用传指针的方法去复制
/// <summary>
/// Copy data from buffer to dst from dst[0]
/// </summary>
/// <param name="dst"></param>
/// <param name="srcIndex"></param>
/// <param name="length"></param>
/// <exception cref="OverflowException"></exception>
public void CopyTo(ref T[] dst, int srcIndex, int length)
{fixed (T* ptr = dst)
{CopyTo(ptr, srcIndex, length);
}
}/// <summary>
/// Copy data from buffer to dst from dst[0]
/// </summary>
/// <param name="dst"></param>
/// <param name="srcIndex"></param>
/// <param name="length"></param>
/// <exception cref="OverflowException"></exception>
public void CopyTo(T* dst, int srcIndex, int length)
{var l = srcIndex + length;//size checkEnsureCapacity(ref l);//copyUnsafe.CopyBlockUnaligned(dst, Data + srcIndex, (uint)length);
}

转Span

Span特别有用,在切割内存之类的地方没有什么比Span更适合的了,所以我们顺便把转Span也支持吧

显示转换

/// <summary>
/// convert an extensible to buffer from start index with provided length
/// </summary>
/// <param name="startIndex"></param>
/// <param name="length"></param>
/// <returns></returns>
public Span<T> AsSpan(int startIndex, int length)
{var l = startIndex + length;//size checkEnsureCapacity(ref l);return new Span<T>(Data + startIndex, length);
}

这样我们可以从指定位置开始讲指定长度个元素转为Span,同时操作返回的Span可以直接操作到我们这个动态扩容数组内的元素上(因为操作Span的元素相当于直接操作内存)

隐式转换

/// <summary>
/// Convert to span
/// </summary>
/// <param name="buffer"></param>
/// <returns></returns>
public static implicit operator Span<T>(ExtensibleBuffer<T> buffer) => buffer.AsSpan(0, buffer.TotalLength);

这里我们从第0个元素开始把当前总长度个元素转Span

转托管数组

因为有可能需要给其他接口使用,所以我们需要能把非托管数组的数据复制到托管数组,只需要new个托管数组然后调用复制的接口即可

/// <summary>
/// Convert buffer data to an Array (will create a new array and copy values)
/// </summary>
/// <param name="startIndex"></param>
/// <param name="length"></param>
/// <returns></returns>
public T[] ToArray(int startIndex, int length)
{T[] ret = new T[length];CopyTo(ref ret, startIndex, length);return ret;
}

完整代码

可以在GitHub上看:Nino,当然我本人更希望大家来点star,也可以看下面贴出的代码:

using System;
using System.Runtime.InteropServices;
using System.Runtime.CompilerServices;namespace Nino.Shared.IO
{/// <summary>/// A buffer that can dynamically extend/// </summary>/// <typeparam name="T"></typeparam>public sealed unsafe class ExtensibleBuffer<T> where T : unmanaged
{/// <summary>/// Default size of the buffer/// </summary>private const int DefaultBufferSize = 128;/// <summary>/// Data that stores everything/// </summary>public T* Data { get; private set; }/// <summary>/// Size of T/// </summary>private readonly byte sizeOfT;/// <summary>/// expand size for each block/// </summary>public readonly int ExpandSize;/// <summary>/// Total length of the buffer/// </summary>public int TotalLength { get; private set; }/// <summary>/// Init buffer/// </summary>public ExtensibleBuffer() : this(DefaultBufferSize){}/// <summary>/// Init buffer/// </summary>public ExtensibleBuffer(int expandSize) : this(expandSize, null){}/// <summary>/// Init extensible buffer with a capacity/// </summary>/// <param name="size"></param>/// <param name="initialData"></param>private ExtensibleBuffer(int size, T[] initialData){sizeOfT = (byte)sizeof(T);ExpandSize = size;Data = (T*)Marshal.AllocHGlobal(sizeOfT * ExpandSize);if (initialData != null){fixed(T* ptr = initialData){CopyFrom(ptr, 0, 0, initialData.Length);}}TotalLength = ExpandSize;GC.AddMemoryPressure(sizeOfT * ExpandSize);}/// <summary>/// Get element at index/// </summary>/// <param name="index"></param>public T this[int index]{[MethodImpl(MethodImplOptions.AggressiveInlining)]get => Data[index];[MethodImpl(MethodImplOptions.AggressiveInlining)]set{EnsureCapacity(ref index);Data[index] = value;}}/// <summary>/// Ensure index exists/// </summary>/// <param name="index"></param>private void EnsureCapacity(ref int index){if (index < TotalLength) return;while (index >= TotalLength){TotalLength += ExpandSize;GC.AddMemoryPressure(sizeOfT * ExpandSize);}Extend();}/// <summary>/// Extend buffer/// </summary>[MethodImpl(MethodImplOptions.AggressiveInlining)]private void Extend(){Data = (T*)Marshal.ReAllocHGlobal((IntPtr)Data, new IntPtr(TotalLength * sizeOfT));}/// <summary>/// Convert buffer data to an Array (will create a new array and copy values)/// </summary>/// <param name="startIndex"></param>/// <param name="length"></param>/// <returns></returns>public T[] ToArray(int startIndex, int length){T[] ret = new T[length];CopyTo(ref ret, startIndex, length);return ret;}/// <summary>/// convert an extensible to buffer from start index with provided length/// </summary>/// <param name="startIndex"></param>/// <param name="length"></param>/// <returns></returns>public Span<T> AsSpan(int startIndex, int length){var l = startIndex + length;//size checkEnsureCapacity(ref l);return new Span<T>(Data + startIndex, length);}/// <summary>/// Convert to span/// </summary>/// <param name="buffer"></param>/// <returns></returns>public static implicit operator Span<T>(ExtensibleBuffer<T> buffer) => buffer.AsSpan(0, buffer.TotalLength);/// <summary>/// Copy data to extensible buffer/// </summary>/// <param name="src"></param>/// <param name="srcIndex"></param>/// <param name="dstIndex"></param>/// <param name="length"></param>/// <exception cref="InvalidOperationException"></exception>public void CopyFrom(T[] src, int srcIndex, int dstIndex, int length){fixed (T* ptr = src){CopyFrom(ptr, srcIndex, dstIndex, length);}}/// <summary>/// Copy data to extensible buffer/// why unaligned? https://stackoverflow.com/a/72418388/// </summary>/// <param name="src"></param>/// <param name="srcIndex"></param>/// <param name="dstIndex"></param>/// <param name="length"></param>/// <exception cref="InvalidOperationException"></exception>public void CopyFrom(T* src, int srcIndex, int dstIndex, int length){var l = dstIndex + length;//size checkEnsureCapacity(ref l);//copyUnsafe.CopyBlockUnaligned(Data + dstIndex, src + srcIndex, (uint)length);}/// <summary>/// Copy data from buffer to dst from dst[0]/// </summary>/// <param name="dst"></param>/// <param name="srcIndex"></param>/// <param name="length"></param>/// <exception cref="OverflowException"></exception>public void CopyTo(ref T[] dst, int srcIndex, int length){fixed (T* ptr = dst){CopyTo(ptr, srcIndex, length);}}/// <summary>/// Copy data from buffer to dst from dst[0]/// </summary>/// <param name="dst"></param>/// <param name="srcIndex"></param>/// <param name="length"></param>/// <exception cref="OverflowException"></exception>public void CopyTo(T* dst, int srcIndex, int length){var l = srcIndex + length;//size checkEnsureCapacity(ref l);//copyUnsafe.CopyBlockUnaligned(dst, Data + srcIndex, (uint)length);}/// <summary>/// Free allocated memories/// </summary>~ExtensibleBuffer(){Marshal.FreeHGlobal((IntPtr)Data);GC.RemoveMemoryPressure(sizeOfT * TotalLength);}
}
}

Benchmark

就这样,我们理论上低GC高性能的非托管动态扩容数组就做好了,让我们分析一下性能,测试代码:

BenchmarkDotNet=v0.13.1, OS=macOS Monterey 12.0.1 (21A559) [Darwin 21.1.0]
Intel Core i9-8950HK CPU 2.90GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET SDK=6.0.301
[Host]   : .NET 6.0.6 (6.0.622.26707), X64 RyuJIT
ShortRun : .NET 6.0.6 (6.0.622.26707), X64 RyuJITJob=ShortRun Platform=AnyCpu Runtime=.NET 6.0  
IterationCount=1 LaunchCount=1 WarmupCount=1

首先我们测试了ExtensibleBuffer和List的无优化版(V1,不指定扩容/初始长度),以及优化版(V2,指定扩容/初始长度)

同时我们测试了byte(1字节)作为泛型类型,以及int(4字节)作为泛型类型

我们先看看100个元素的插入:

MethodtestCountMeanErrorGen 0Gen 1Gen 2Allocated
ByteExtensibleBufferInsertV1100466.7 nsNA0.02770.02770.027740 B
ByteExtensibleBufferInsertV2100440.4 nsNA0.02190.02190.021940 B
ByteListInsertV1100273.6 nsNA0.0687--432 B
ByteListInsertV2100173.2 nsNA0.0253--160 B
IntExtensibleBufferInsertV1100663.2 nsNA0.11730.11730.117340 B
IntExtensibleBufferInsertV2100645.4 nsNA0.08580.08580.085840 B
IntListInsertV1100299.2 nsNA0.1884--1,184 B
IntListInsertV2100192.1 nsNA0.0725--456 B

为什么会比List略慢?因为申请内存是有耗时的,虽然基本无感知。不过GC的优化是不是挺不错的?

我们现在看看1000个元素的插入:

MethodtestCountMeanErrorGen 0Gen 1Gen 2Allocated
ByteExtensibleBufferInsertV110003,323.6 nsNA0.23270.23270.232740 B
ByteExtensibleBufferInsertV210001,560.3 nsNA0.23650.23650.236540 B
ByteListInsertV110001,917.9 nsNA0.3643--2,296 B
ByteListInsertV210001,554.6 nsNA0.1678--1,056 B
IntExtensibleBufferInsertV110003,080.0 nsNA0.96890.96890.968941 B
IntExtensibleBufferInsertV21000989.2 nsNA0.92510.92510.925141 B
IntListInsertV110002,445.4 nsNA1.3390--8,424 B
IntListInsertV210001,868.7 nsNA0.6447--4,056 B

速度是不是基本一样了?但是GC是不是少了特别特别多?

现在看看1000以上的元素的插入:

MethodtestCountMeanErrorGen 0Gen 1Gen 2Allocated
ByteExtensibleBufferInsertV11000025,683.9 nsNA2.34992.34992.349942 B
ByteExtensibleBufferInsertV21000011,535.0 nsNA2.34992.34992.349942 B
ByteListInsertV11000017,051.6 nsNA5.2490--33,112 B
ByteListInsertV21000016,544.9 nsNA1.5869--10,056 B
IntExtensibleBufferInsertV11000025,945.4 nsNA9.58259.58259.582546 B
IntExtensibleBufferInsertV2100009,269.5 nsNA8.23978.23978.239746 B
IntListInsertV11000023,988.9 nsNA20.8130--131,400 B
IntListInsertV21000016,521.5 nsNA6.3477--40,056 B
ByteExtensibleBufferInsertV1100000276,784.6 nsNA22.949222.949222.949256 B
ByteExtensibleBufferInsertV2100000121,097.0 nsNA23.559623.559623.559656 B
ByteListInsertV1100000247,649.4 nsNA205.3223205.322334.4238262,583 B
ByteListInsertV2100000213,715.3 nsNA161.1328161.132826.8555100,074 B
IntExtensibleBufferInsertV1100000244,882.5 nsNA93.750093.750093.7500109 B
IntExtensibleBufferInsertV2100000111,195.8 nsNA86.303786.303786.303782 B
IntListInsertV1100000533,471.8 nsNA619.1406619.1406233.39841,049,161 B
IntListInsertV2100000326,374.4 nsNA265.6250265.625099.6094400,123 B
ByteExtensibleBufferInsertV110000002,656,296.6 nsNA226.5625226.5625226.5625195 B
ByteExtensibleBufferInsertV210000001,214,632.2 nsNA197.2656197.2656197.2656174 B
ByteListInsertV110000002,422,943.3 nsNA1394.53131394.5313398.43752,097,906 B
ByteListInsertV210000001,636,061.4 nsNA207.0313207.0313197.26561,000,185 B
IntExtensibleBufferInsertV110000003,663,844.0 nsNA851.5625851.5625851.5625547 B
IntExtensibleBufferInsertV21000000857,195.9 nsNA498.0469498.0469498.0469377 B
IntListInsertV110000003,717,760.8 nsNA1054.68751039.06251000.00008,389,735 B
IntListInsertV210000002,265,089.4 nsNA511.7188511.7188492.18754,000,381 B
ByteExtensibleBufferInsertV11000000029,853,310.2 nsNA1656.25001656.25001656.25001,178 B
ByteExtensibleBufferInsertV21000000010,881,063.5 nsNA984.3750984.3750984.3750714 B
ByteListInsertV11000000030,683,668.0 nsNA3312.50003312.50001625.000033,556,204 B
ByteListInsertV21000000016,752,229.2 nsNA593.7500593.7500437.500010,000,366 B
IntExtensibleBufferInsertV11000000052,335,791.2 nsNA2500.00002500.00002500.00001,802 B
IntExtensibleBufferInsertV2100000008,783,753.0 nsNA984.3750984.3750984.3750714 B
IntListInsertV11000000078,802,672.6 nsNA5142.85715142.85713000.0000134,220,415 B
IntListInsertV21000000033,037,550.0 nsNA937.5000937.5000937.500040,001,345 B
ByteExtensibleBufferInsertV1100000000297,324,344.5 nsNA5000.00005000.00005000.00003,808 B
ByteExtensibleBufferInsertV2100000000113,086,965.2 nsNA800.0000800.0000800.0000741 B
ByteListInsertV1100000000303,881,242.5 nsNA5500.00005500.00003000.0000268,438,564 B
ByteListInsertV2100000000172,889,432.0 nsNA666.6667666.6667666.6667100,002,269 B
IntExtensibleBufferInsertV1100000000394,704,429.0 nsNA12000.000012000.000012000.00009,536 B
IntExtensibleBufferInsertV210000000077,565,079.3 nsNA1000.00001000.00001000.0000848 B
IntListInsertV1100000000690,861,266.0 nsNA8000.00008000.00003000.00001,073,746,576 B
IntListInsertV2100000000310,024,197.0 nsNA500.0000500.0000500.0000400,001,880 B

速度是不是快了好几倍(毕竟直接在指针上复制会快很多,也少了托管数组分配的耗时),GC是不是少了几千、几万、几十万倍?

使用场景

有人可能会问,这玩意儿有使用场景吗?

答案是有的,且很多。

  • 基本上用Stream持续写入二进制数据的使用场景都很契合这个非托管动态扩容数组(如网络IO),因为这种IO都是KB/MB/GB级别的,而在这个量级下,该动态扩容数组有着出色的性能和卓越的GC优化

  • 序列化这种需要不断写入数据的场所也很契合

  • 填充加密的使用情况也很适合(比如把二进制数据每n字节之间插入m字节的假数据,最后再转托管byte数组返回出去,可以用这个动态扩容数组在塞入假数据期间实现无GC高性能处理)

  • TCP粘包处理也很契合(类似上面提到的网络IO,但是不太一样,因为要不断地Enqueue二进制数据到扩容数组,然后如果满足包头记录的总长度了,就Dequeue出去,把后面的内容移动到最前面,以后会有这个方案的文章)

  • 还有很多很多的用途,比如通过非托管动态扩容数组写数据,然后用其非托管数据的指针,传递给C/C++等原生代码去实现无GC的高性能功能(这个以后也会有文章,关于搭配这个和Zlib native的文章)

最后

为什么不是无GC非托管动态扩容数组呢?因为我们这个数组是个对象,所以造成GC。

特别感谢阅读到最后的朋友,希望能给大家带来帮助,以后我还会写一个收集对象的内存地址,转IntPtr实现的低GC托管动态扩容数组。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/283015.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

leetCode 53. maximum subarray

Find the contiguous subarray within an array (containing at least one number) which has the largest sum. For example, given the array [-2,1,-3,4,-1,2,1,-5,4],the contiguous subarray [4,-1,2,1] has the largest sum 6. 贪婪算法找每个当前位置对应的最大的subar…

如何成为有效学习的高手(许岑)——思维导图

总结自许岑精品课《如何成为有效学习的高手》&#xff0c;图片看不清的可以看下面。 最后有彩蛋&#xff01;最后有彩蛋&#xff01;最后有彩蛋&#xff01; 定义 高效学习的定义&#xff1a;找到最适合自己的学习手法&#xff0c;在相对短的时间内集中注意力&#xff0c;以解决…

tomcat不能多次startup.sh,异常时直接,分析logs目录下的日志。

tomcat不能多次startup.sh&#xff0c;异常时直接干掉其进程。 分析logs目录下的日志。

java类sample是公共的_应在名samle.java的文件_Andoid NDK编程 1 - 注册native函数

打算对Android的NDK的开发做一总结&#xff0c;首先是JNI部分&#xff0c;接下来是NDK的内容。今天首先介绍一下JNI的第一部分&#xff1a;注册native函数。当java代码中执行native的代码时候&#xff0c;首先是通过一定的方法来找到这些native方法。而注册native函数的具体方法…

WPF Canvas 平滑笔迹

WPF Canvas 平滑笔迹控件名&#xff1a;CanvasHandWriting作者&#xff1a;小封&#xff08;邝攀升&#xff09;原文链接&#xff1a; https://github.com/WPFDevelopersOrg/WPFDevelopers编辑&#xff1a;驚鏵完整的思路如下收集路径点集。平均采样路径点集。将路径点集转为…

IIS应用程序池相关问题及连接池已满的解决方法

关于应用程序池 在 IIS 6.0 中,引入了应用程序池&#xff0c;应用程序池是将一个或多个应用程序链接到一个或多个工作进程集合的配置。因为应用程序池中的应用程序与其他应用程序被工作进程边界分隔&#xff0c;所以某个应用程序池中的应用程序不会受到其他应用程序池中应用程序…

echo -n 和echo -e 参数意义

echo -n 不换行输出 $echo -n "123" $echo "456" 12最终输出 123456而不是 123 456 123456echo -e 处理特殊字符 若字符串中出现以下字符&#xff0c;则特别加以处理&#xff0c;而不会将它当成一般文字输出&#xff1a; \a 发出警告声&#xff1b; \b 删…

NetSpeed

NetSpeed公司提供的NOC包括三部分&#xff0c;可以通过NocStudio进行配置生成。 1)NetSpeed Orion&#xff0c;面向快速SoC design的可综合平台。 2)Linley NetSpeed NoC面向复杂的interconnect实现&#xff0c;同时优化内部physical implementation和timing closure. NoC是基于…

js ajax java传参_ajax参数传递与后台接收

ajax参数传递与后台接收Servlet中读取http参数的方法Enumeration getParameterNames() 返回一个 String 对象的枚举&#xff0c;包含在该请求中包含的参数的名称String getParameter(String name) 以字符串形式返回请求参数的值&#xff0c;或者如果参数不存在则返回 null。Str…

init 访问器只能初始化时赋值,是真的吗?

前言C# 提供的 init 关键字用于在属性中定义访问器方法&#xff0c;可以让属性仅能在对象初始化的时候被赋值&#xff0c;其他时候只能为只读属性的形式。例如下面代码可以正常执行&#xff1a;public class Demo {public string Name { get; init; } }var demo new Demo { Na…

eclipse实现代码块折叠-类似于VS中的#region……#endregion

背 景 刚才在写代码的时候&#xff0c;写了十几行可以说是重复的代码&#xff1a; 如果整个方法或类中代码多了&#xff0c;感觉它们太TM占地方了&#xff0c;给读者在阅读代码上造成很大的困难&#xff0c;于是想到能不能把他们“浓缩”成一行&#xff0c;脑子里第一个闪现出的…

添加Chrome插件(Github上下载的压缩文件)

首先把压缩包解压到某个文件夹 然后按照以下步骤进行即可&#xff1a; 点击Chrome浏览器上的设置->扩展程序->开发者模式->点击加载已解压的压缩文件->选中解压过的文件夹确定即可。转载于:https://www.cnblogs.com/yijianzhongqing/p/6277838.html

java定义基础变量语句_java语言基础-变量

一丶变量的基本概念1.什么是变量(1).内存中的一个存储区域(2).该区域有自己的名称(变量名),和类型(数据类型)(3.)该区域的数据可以在同一类型范围内不断变化(定义变量的主要目的是因为数据的不确定性)2.为什么要定义变量用来不断存放同一类型的常量&#xff0c;并可以重复使用3…

C# WPF MVVM模式[经典]案例

01—前言Caliburn.Micro(简称CM)一经推出便备受推崇&#xff0c;作为一款MVVM开发模式的经典框架&#xff0c;越来越多的受到wpf开发者的青睐.我们看一下官方的描述&#xff1a;Caliburn是一个为Xaml平台设计的小型但功能强大的框架。Micro实现了各种UI模式&#xff0c;用于解决…

shell数组

定义数组[rootwy shell]# a(1 2 3 4)显示数组[rootwy shell]# echo ${a[]}1 2 3 4[rootwy shell]# echo ${a[*]}1 2 3 4显示数组中的某个元素[rootwy shell]# echo ${a[0]}1增加元素[rootwy shell]# a[4]9[rootwy shell]# echo ${a[*]}1 2 3 4 9修改元素值 [rootwy shell]# a[2…

java二级程序题两个角度_两个角度图_【SCME大一】使用JAVA语言深入理解程序逻辑答案_学小易找答案...

【填空题】《蝶恋花 伫倚危楼风细细 》的作者( )。【简答题】简要概述问卷调查的整体设计?【填空题】父母在,( ),游必有方。【填空题】白居易与刘禹锡并称“( )”。【填空题】白居易,字( )。【填空题】白居易,是唐代伟大的( )主义诗人。【单选题】《红楼梦》最成功处在于塑造了…

LINUX中常用操作命令

LINUX中常用操作命令 引用&#xff1a;http://www.daniubiji.cn/archives/25 Linux简介及Ubuntu安装 常见指令系统管理命令打包压缩相关命令关机/重启机器Linux管道Linux软件包管理vim使用用户及用户组管理文件权限管理Linux简介及Ubuntu安装 Linux&#xff0c;免费开源&#x…

Log4j编写

来自: http://www.blogjava.net/zJun/archive/2006/06/28/55511.html Log4J的配置文件(Configuration File)就是用来设置记录器的级别、存放器和布局的&#xff0c;它可接keyvalue格式的设置或xml格式的设置信息。通过配置&#xff0c;可以创建出Log4J的运行环境。1. 配置文件L…

C# 为什么高手喜欢用StartsWith而不是Substring进行字符串匹配?

字符串的截取匹配操作在开发中非常常见&#xff0c;比如下面这个示例&#xff1a;我要匹配查找出来字符串数组中以“abc”开头的字符串并打印&#xff0c;我下面分别用了两种方式实现&#xff0c;代码如下&#xff1a;using System;namespace ConsoleApp23 {class Program{stat…

Nginx 服务器开启status页面检测服务状态

原文&#xff1a;http://www.cnblogs.com/hanyifeng/p/5830013.html 一、Nginx status monitor 和apache 中服务器状态一样。输出的内容如&#xff1a; 第1列&#xff1a; 当前与http建立的连接数&#xff0c;包括等待的客户端连接&#xff1a;2第2列&#xff1a;接受的客户端连…