.NET內存包裝類 Memory 和 Span 相關類型

2021-03-02 DotNet開發跳槽

作者: 大師兄石頭
來源: https://bigbrotherstone.cnblogs.com/

1. 前言

2. 簡介

3. Memory<T>和Span<T>使用準則

1. 前言

此文章是官方文檔的翻譯,由於官方文檔中文版是機器翻譯的,有些部分有疏漏和錯誤,所以本人進行了翻譯供大家學習,如有問題歡迎指正。

參考資料
memory-and-spans --- Microsoft

2. 簡介

.NET 包含多個相互關聯的類型,它們表示任意內存的連續的強類型區域。這些方法包括:

System.Span<T>、System.Memory<T> 及其對應的只讀類型被設計為:

備註:
對於早期框架,Span<T> 和 Memory<T> 在 System.Memory NuGet 包中提供。

使用 memory 和 span

3. Memory<T>和Span<T>使用準則

Span<T> 和 Memory<T> 都是可用於 pipeline 的結構化數據的緩衝區。

3.1. 所有者, 消費者和生命周期管理

由於可以在各個 API 之間傳送緩衝區,以及由於緩衝區有時可以從多個線程進行訪問,因此請務必考慮生命周期管理。下面介紹三個核心概念:

以下偽代碼示例闡釋了這三個概念。它包括:

實例化類型為 Char 的 Memory<T> 緩衝區的

調用 WriteInt32ToBuffer 方法以將整數的字符串表示形式寫入緩衝區

然後調用 DisplayBufferToConsole 方法以顯示緩衝區的值。

using System;

class Program
{
// Write 'value' as a human-readable string to the output buffer.
void WriteInt32ToBuffer(int value, Buffer buffer);

// Display the contents of the buffer to the console.
void DisplayBufferToConsole(Buffer buffer);

// Application code
static void Main()
{
var buffer = CreateBuffer();
try
{
int value = Int32.Parse(Console.ReadLine());
WriteInt32ToBuffer(value, buffer);
DisplayBufferToConsole(buffer);
}
finally
{
buffer.Destroy();
}
}
}

所有者

消費者

先是 WriteInt32ToBuffer ,然後是 DisplayBufferToConsole

WriteInt32ToBuffer 和 DisplayBufferToConsole

一次只能有一個消費者

這兩個消費者都不擁有緩衝區

此上下文中的「消費者」並不意味著以只讀形式查看緩衝區;如果提供了以讀/寫形式查看緩衝區的權限,則消費者可以像 WriteInt32ToBuffer 那樣修改緩衝區的內容

租約

3.2. Memory<T> 和所有者/消費者模型

.NET Core 支持以下兩種所有權模型:

使用 System.Buffers.IMemoryOwner<T> 接口顯式的管理緩衝區的所有權。

IMemoryOwner<T> 支持上述這兩種所有權模型

具有 IMemoryOwner<T> 引用的組件擁有緩衝區

以下示例使用 IMemoryOwner<T> 實例反映 Memory<T> 緩衝區的所有權。

using System;
using System.Buffers;

class Example
{
static void Main()
{
IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent();

Console.Write("Enter a number: ");
try {
var value = Int32.Parse(Console.ReadLine());

var memory = owner.Memory;

WriteInt32ToBuffer(value, memory);

DisplayBufferToConsole(owner.Memory.Slice(0, value.ToString().Length));
}
catch (FormatException) {
Console.WriteLine("You did not enter a valid number.");
}
catch (OverflowException) {
Console.WriteLine($"You entered a number less than {Int32.MinValue:N0} or greater than {Int32.MaxValue:N0}.");
}
finally {
owner?.Dispose();
}
}

static void WriteInt32ToBuffer(int value, Memory<char> buffer)
{
var strValue = value.ToString();

var span = buffer.Span;
for (int ctr = 0; ctr < strValue.Length; ctr++)
span[ctr] = strValue[ctr];
}

static void DisplayBufferToConsole(Memory<char> buffer) =>
Console.WriteLine($"Contents of the buffer: '{buffer}'");
}

也可以使用 using 編寫此示例:

using System;
using System.Buffers;

class Example
{
static void Main()
{
using (IMemoryOwner<char> owner = MemoryPool<char>.Shared.Rent())
{
Console.Write("Enter a number: ");
try {
var value = Int32.Parse(Console.ReadLine());

var memory = owner.Memory;
WriteInt32ToBuffer(value, memory);
DisplayBufferToConsole(memory.Slice(0, value.ToString().Length));
}
catch (FormatException) {
Console.WriteLine("You did not enter a valid number.");
}
catch (OverflowException) {
Console.WriteLine($"You entered a number less than {Int32.MinValue:N0} or greater than {Int32.MaxValue:N0}.");
}
}
}

static void WriteInt32ToBuffer(int value, Memory<char> buffer)
{
var strValue = value.ToString();

var span = buffer.Slice(0, strValue.Length).Span;
strValue.AsSpan().CopyTo(span);
}

static void DisplayBufferToConsole(Memory<char> buffer) =>
Console.WriteLine($"Contents of the buffer: '{buffer}'");
}

在此代碼中:

儘管 WriteInt32ToBuffer 方法用於將數據寫入緩衝區,但 DisplayBufferToConsole 方法並不如此。

3.3. 「缺少所有者」 的Memory<T> 實例

無需使用 IMemoryOwner<T> 即可創建 Memory<T> 實例。在這種情況下,緩衝區的所有權是隱式的,並且僅支持單所有者模型。可以通過以下方式達到此目的:

using System;

class Example
{
static void Main()
{
Memory<char> memory = new char[64];

Console.Write("Enter a number: ");
var value = Int32.Parse(Console.ReadLine());

WriteInt32ToBuffer(value, memory);
DisplayBufferToConsole(memory);
}

static void WriteInt32ToBuffer(int value, Memory<char> buffer)
{
var strValue = value.ToString();
strValue.AsSpan().CopyTo(buffer.Slice(0, strValue.Length).Span);
}

static void DisplayBufferToConsole(Memory<char> buffer) =>
Console.WriteLine($"Contents of the buffer: '{buffer}'");
}

3.4. 使用準則

因為擁有一個內存塊,但打算將其傳遞給多個組件,其中一些組件可能同時在特定的內存塊上運行,所以建立使用Memory<T>和Span<T>的準則是很必要的,因為:

所有者釋放它之後,一個組件還可能會保留對該存儲塊的引用。

兩個組件可能並發的同時在緩衝區上進行操作,從而破壞了緩衝區中的數據。

儘管Span<T>的堆棧分配性質優化了性能,而且使Span<T>成為在內存塊上運行的首選類型,但它也使Span<T>受到一些主要限制

下面介紹成功使用 Memory<T> 及其相關類型的建議。除非另有明確說明,否則適用於 Memory<T> 和 Span<T> 的指南也適用於 ReadOnlyMemory<T> 和 ReadOnlySpan<T> 。

規則 1:對於同步 API,如有可能,請使用 Span<T>(而不是 Memory<T>)作為參數。

Span<T> 比 Memory<T> 更多功能:

可以表示更多種類的連續內存緩衝區

Span<T> 還提供比 Memory<T> 更好的性能

無法進行 Span<T> 到 Memory<T> 的轉換

可以使用 Memory<T>.Span 屬性將 Memory<T> 實例轉換為 Span<T>

使用類型 Span<T>(而不是類型 Memory<T>)作為方法的參數類型還可以幫助你編寫正確的消費方法實現。你將自動進行編譯時檢查,以確保不會企圖訪問此方法租約之外的緩衝區

有時,必須使用 Memory<T> 參數(而不是 Span<T> 參數),即使完全同步也是如此。所依賴的 API 可能僅接受 Memory<T> 參數。這沒有問題,但當使用同步的 Memory<T> 時,應注意權衡利弊

規則 2:如果緩衝區應為只讀,則使用 ReadOnlySpan<T> 或 ReadOnlyMemory<T>

在前面的示例中,DisplayBufferToConsole 方法僅從緩衝區讀取數據;它不修改緩衝區的內容。方法籤名應進行修改如下。

void DisplayBufferToConsole(ReadOnlyMemory<char> buffer);

事實上,如果我們結合 規則1 和 規則2 ,我們可以做得更好,並重寫方法籤名如下:

void DisplayBufferToConsole(ReadOnlySpan<char> buffer);

DisplayBufferToConsole 方法現在幾乎適用於每一個能夠想到的緩衝區類型:

規則 3:如果方法接受 Memory<T> 並返回 void,則該方法的代碼中return之後不得使用 Memory<T> 實例,保證方法結束後對其使用也結束。

這與前面提到的「租約」概念相關。返回 void 的方法對 Memory<T> 實例的租用將在進入該方法時開始,並在退出該方法時結束。請考慮以下示例,該示例會基於控制臺中的輸入在循環中調用 Log。

using System;
using System.Buffers;

public class Example
{
// implementation provided by third party
static extern void Log(ReadOnlyMemory<char> message);

// user code
public static void Main()
{
using (var owner = MemoryPool<char>.Shared.Rent())
{
var memory = owner.Memory;
var span = memory.Span;
while (true)
{
int value = Int32.Parse(Console.ReadLine());
if (value < 0)
return;

int numCharsWritten = ToBuffer(value, span);
Log(memory.Slice(0, numCharsWritten));
}
}
}

private static int ToBuffer(int value, Span<char> span)
{
string strValue = value.ToString();
int length = strValue.Length;
strValue.AsSpan().CopyTo(span.Slice(0, length));
return length;
}
}

如果 Log 是完全同步的方法,則此代碼將按預期運行,因為在任何給定時間只有一個活躍的內存實例消費者。但是,請想像Log具有此實現。

// !!! INCORRECT IMPLEMENTATION !!!
static void Log(ReadOnlyMemory<char> message)
{
// Run in background so that we don't block the main thread while performing IO.
Task.Run(() =>
{
StreamWriter sw = File.AppendText(@".\input-numbers.dat");
sw.WriteLine(message);
});
}

在此實現中,Log 違反租約,因為它在 return 之後仍嘗試在後臺使用 Memory<T> 實例。Main 方法可能會在 Log 嘗試從緩衝區進行讀取時更改緩衝區數據,這可能導致消費者在使用緩存區數據時數據已經被修改。

有多種方法可解決此問題:

Log 方法可以按以下所示,返回 Task,而不是 void。

// An acceptable implementation.
static Task Log(ReadOnlyMemory<char> message)
{
// Run in the background so that we don't block the main thread while performing IO.
return Task.Run(() => {
StreamWriter sw = File.AppendText(@".\input-numbers.dat");
sw.WriteLine(message);
sw.Flush();
});
}

也可以改為按如下所示實現 Log:

// An acceptable implementation.
static void Log(ReadOnlyMemory<char> message)
{
string defensiveCopy = message.ToString();
// Run in the background so that we don't block the main thread while performing IO.
Task.Run(() => {
StreamWriter sw = File.AppendText(@".\input-numbers.dat");
sw.WriteLine(defensiveCopy);
sw.Flush();
});
}

規則 4:如果方法接受 Memory<T> 並返回某個Task,則在Task轉換為終止狀態之前不得使用 Memory<T> 實例。

這個是 規則3 的異步版本。以下示例是遵守此規則,按上面例子編寫的 Log 方法:

// An acceptable implementation.
static Task Log(ReadOnlyMemory<char> message)
{
// Run in the background so that we don't block the main thread while performing IO.
return Task.Run(() => {
string defensiveCopy = message.ToString();
StreamWriter sw = File.AppendText(@".\input-numbers.dat");
sw.WriteLine(defensiveCopy);
sw.Flush();
});
}

此處的「終止狀態」表示任務轉換為 completed, faulted, canceled 狀態。

此指南適用於返回 Task、Task<TResult>、ValueTask<TResult> 或任何類似類型的方法。

規則5:如果構造函數接受Memory <T>作為參數,則假定構造對象上的實例方法是Memory<T>實例的消費者。

請看以下示例:

class OddValueExtractor
{
public OddValueExtractor(ReadOnlyMemory<int> input);
public bool TryReadNextOddValue(out int value);
}

void PrintAllOddValues(ReadOnlyMemory<int> input)
{
var extractor = new OddValueExtractor(input);
while (extractor.TryReadNextOddValue(out int value))
{
Console.WriteLine(value);
}
}

此處的 OddValueExtractor 構造函數接受 ReadOnlyMemory<int> 作為構造函數參數,因此構造函數本身是 ReadOnlyMemory<int> 實例的消費者,並且該實例的所有實例方法也是原始 ReadOnlyMemory<int> 實例的消費者。這意味著 TryReadNextOddValue 消費 ReadOnlyMemory<int> 實例,即使該實例未直接傳遞到 TryReadNextOddValue 方法。

規則 6:如果一個類型具有可寫的 Memory<T> 類型的屬性(或等效的實例方法),則假定該對象上的實例方法是 Memory<T> 實例的消費者。

這是 規則5 的變體。之所以存在此規則,是因為假定使用了可寫屬性或等效方法來捕獲並保留輸入的 Memory<T> 實例,因此同一對象上的實例方法可以利用捕獲的實例。

以下示例觸發了此規則:

class Person
{
// Settable property.
public Memory<char> FirstName { get; set; }

// alternatively, equivalent "setter" method
public SetFirstName(Memory<char> value);

// alternatively, a public settable field
public Memory<char> FirstName;
}

規則 7:如果具有 IMemoryOwner<T> 的引用,則必須在某些時候對其進行處理或轉讓其所有權(但不同時執行兩個操作)。

由於 Memory<T> 實例可能由託管或非託管內存提供支持,因此在對 Memory<T> 實例執行的工作完成之後,所有者必須調用 MemoryPool<T>.Dispose。

此外,所有者可能會將 IMemoryOwner<T> 實例的所有權轉讓給其他組件,同時獲取所有權的組件將負責在適當時間調用 MemoryPool<T>.Dispose

調用 Dispose 方法失敗可能會導致非託管內存洩漏或其他性能降低問題

此規則也適用於調用工廠方法的代碼(如 MemoryPool<T>.Rent)。調用方將成為工廠生產的 IMemoryOwner<T> 的所有者,並負責在完成後 Dispose 該實例。

規則 8:如果 API 接口中具有 IMemoryOwner<T> 參數,即表示你接受該實例的所有權。

接受此類型的實例表示組件打算獲取此實例的所有權。該組件將負責根據 規則7 進行正確處理。

在方法調用完成後,將 IMemoryOwner<T> 實例的所有權轉讓給其他組件,之後該組件將不再使用該實例。

重要:
構造函數接受 IMemoryOwner<T> 作為參數的類應實現接口 IDisposable,並且 Dispose 方法中應調用 MemoryPool<T>.Dispose。

規則 9:如果要封裝同步的 p/invoke 方法,則應接受 Span<T> 作為參數

根據 規則1,Span<T> 通常是用於同步 API 的合規類型。可以通過 fixed 關鍵字固定 Span<T> 實例,如下面的示例所示。

using System.Runtime.InteropServices;

[DllImport(...)]
private static extern unsafe int ExportedMethod(byte* pbData, int cbData);

public unsafe int ManagedWrapper(Span<byte> data)
{
fixed (byte* pbData = &MemoryMarshal.GetReference(data))
{
int retVal = ExportedMethod(pbData, data.Length);

/* error checking retVal goes here */

return retVal;
}
}

在上一示例中,如果輸入 span 為空,則 pbData 可以為 Null。如果 ExportedMethod 方法參數 pbData 不能為 Null,可以按如下示例實現該方法:

public unsafe int ManagedWrapper(Span<byte> data)
{
fixed (byte* pbData = &MemoryMarshal.GetReference(data))
{
byte dummy = 0;
int retVal = ExportedMethod((pbData != null) ? pbData : &dummy, data.Length);

/* error checking retVal goes here */

return retVal;
}
}

規則 10:如果要包裝異步 p/invoke 方法,則應接受 Memory<T> 作為參數

由於 fixed 關鍵字不能在異步操作中使用,因此使用 Memory<T>.Pin 方法固定 Memory<T> 實例,無論實例代表的連續內存是哪種類型。下面的示例演示了如何使用此 API 執行異步 p/invoke 調用。

using System.Runtime.InteropServices;

[UnmanagedFunctionPointer(...)]
private delegate void OnCompletedCallback(IntPtr state, int result);

[DllImport(...)]
private static extern unsafe int ExportedAsyncMethod(byte* pbData, int cbData, IntPtr pState, IntPtr lpfnOnCompletedCallback);

private static readonly IntPtr _callbackPtr = GetCompletionCallbackPointer();

public unsafe Task<int> ManagedWrapperAsync(Memory<byte> data)
{
// setup
var tcs = new TaskCompletionSource<int>();
var state = new MyCompletedCallbackState
{
Tcs = tcs
};
var pState = (IntPtr)GCHandle.Alloc(state);

var memoryHandle = data.Pin();
state.MemoryHandle = memoryHandle;

// make the call
int result;
try
{
result = ExportedAsyncMethod((byte*)memoryHandle.Pointer, data.Length, pState, _callbackPtr);
}
catch
{
((GCHandle)pState).Free(); // cleanup since callback won't be invoked
memoryHandle.Dispose();
throw;
}

if (result != PENDING)
{
// Operation completed synchronously; invoke callback manually
// for result processing and cleanup.
MyCompletedCallbackImplementation(pState, result);
}

return tcs.Task;
}

private static void MyCompletedCallbackImplementation(IntPtr state, int result)
{
GCHandle handle = (GCHandle)state;
var actualState = (MyCompletedCallbackState)(handle.Target);
handle.Free();
actualState.MemoryHandle.Dispose();

/* error checking result goes here */

if (error)
{
actualState.Tcs.SetException(...);
}
else
{
actualState.Tcs.SetResult(result);
}
}

private static IntPtr GetCompletionCallbackPointer()
{
OnCompletedCallback callback = MyCompletedCallbackImplementation;
GCHandle.Alloc(callback); // keep alive for lifetime of application
return Marshal.GetFunctionPointerForDelegate(callback);
}

private class MyCompletedCallbackState
{
public TaskCompletionSource<int> Tcs;
public MemoryHandle MemoryHandle;
}

注:
Memory<T>.Pin 方法返回內存句柄,且垃圾回收器將不會移動此處內存,直到釋放該方法返回的 MemoryHandle 對象為止。這使您可以檢索和使用該內存地址。

版權申明:本文來源於網友收集或網友提供,如果有侵權,請轉告版主或者留言,本公眾號立即刪除。

相關焦點

  • .net下的span和memory
    .net core 2.1的重頭戲就是性能,其中最重要的兩個類就是span和memory,本文這裡簡單的介紹一下這兩個類的使用。
  • C/C++程序內存問題分析
    在《C/C++程序core dump問題分析》[1]中,我們分析了程序出core的問題的類型和幾類原因。
  • 使用SQL Server In-Memory存儲ASP.NET的會話狀態
    會話存儲的持續性是通過內存優化表而不是磁碟表.對於繁重的訪問模型,如存儲會話狀態等,內存優化表是全事務的,可持續性和理想的.這類表使用無鎖數據結構和優化的,多版本的並發控制. 為了更進一步提升性能,可用本地編譯存儲過程來恢復和存儲會話數據. 本質上,這是一種新型的存儲過程,它被編譯為本地機器代碼.
  • JS(Memory Management) 內存管理
    每當賦值變量或者創建函數,內存都會經歷如下的過程:分配內存JS引擎為我們分配我們需要操作對象的對應空間回收內存JS引擎通過特定的算法判斷內存是否需要釋放,而被釋放的內存用於為其他操作提供空間。在內存管理器中的對象不僅僅包括JS對象,而且還囊括了函數和函數作用域。
  • 【藏經閣】C/C++程序性能案例分析-內存問題分析
    在C/C++程序中,所有的內存空間一般都是通過指針(pointer)進行引用的,一旦指針丟失,則失去了申請內存的訪問入口,那塊內存就「丟失」了,也就是我們說的洩露。對於一個有一定編程經驗的碼農來說,直接的指針丟失發生的情況比較少。這種類型問題的發生,往往都和程序的框架、複雜的數據結構有關。比如有這樣一個例子,由於開發者對程序框架不了解,導致了內存洩漏。
  • 理解memory barrier
    memory ordered又和speculative execution(預測執行)有極大關係。比如action和port並非是內存地址裡的數據,而是硬體設備的地址或者即使是內存地址的數據但涉及到DMA區域,則會出現至少兩個觀察者。一個是CPU本身,一個是磁碟驅動等外設(實際也可以理解為是一個CPU)。
  • Java內存分配和String類型的深度解析
    本地方法區存在一塊特殊的內存區域,叫常量池(Constant Pool),這塊內存將與String類型的分析密切相關。新生代的大小設置有2個相關參數:老年代(Old Generation): 當 OLD 區空間不夠時, JVM 會在 OLD 區進行 major collection;完全垃圾收集後,若Survivor及OLD區仍然無法存放從Eden複製過來的部分對象,導致JVM無法在Eden區為新對象創建內存區域,則出現」Out of memory錯誤」 。
  • Java基礎,基礎類型包裝類、Math類、枚舉簡介
    一、基礎類型包裝類;之前講解了基礎類型,現在又來一個基礎類型包裝類,那這個基礎類型包裝類是什麼意思呢?基礎類型包括六種數字類型(4個整數類型,2個浮點型),一種字符型,一種boolean型,每種基礎類型都對應的有包裝類,包裝類就是可以將基礎類型包裝成一個對象的類,所以是基礎類型包裝類,對應關係見下圖;基礎包裝類有什麼用法呢?
  • 絕地求生爆內存怎麼辦 內存不足out of memory解決教程
    今天小編為大家帶來的便是關於遊戲中玩家電腦內存夠的情況下提示內存不足out of memory解決教程,... 絕地求生被玩家吐槽最多的應該就是優化問題,雖然官方對於優化問題非常重視,目前還是有非常多的問題。
  • .NET Standard 2.1 發布,進一步使 .NET 實現一致
    在 .NET Core 2.1 中,開發團隊添加了一個類似於數組的類型 Span<T>,它允許以統一的方式表示託管和非託管內存,並支持切片而無需複製。它是 .NET Core 2.1 中與性能相關的大多數改進的核心。由於它允許以更有效的方式管理緩衝區,因此可以幫助減少內存分配和複製。Span<T> 被認為是一種非常基礎的類型,因為它需要運行時和編譯器支持才能充分利用。
  • 初級數據分析 第二篇:改變數據類型減少內存
    用 .memory_usage 方法查找各列的內存使用情況。最後,讓我們比較一下原始內存使用量和更新後的內存使用量。RELAFFIL 列如預期的那樣,只有原來的八分之一大小,而 STABBR 列則縮減到只有原來的百分之三。
  • 手機內存科普:你說的「內存」,其實不是內存?
    內存,英文縮寫為RAM(Random Access Memory,隨機存取存儲器),內存又稱主存(Main memory),即手機(電腦)內部最主要的存儲器,內存從磁碟(也就是手機中的快閃記憶體)中加載各種應用和數據,而CPU需要從內存中讀出程序才能運行。一般來說,存儲在磁碟中的程序必須加載到內存中才能運行。
  • 走進C++11(四十)最寬鬆的順序 memory_order_relaxed 內存模型(三)
    之前講的都是理論相關的,下面詳細講一下我們現實中會使用到的內存模型。
  • 理解 Go 內存管理之內存分配
    每一個span的大小和span中元素的個數都不是固定的,例如span 65 級別的span大小為57344位元組,每一個對象為28672位元組,元素個數為2個。Span大小雖然不固定,但其是8K或更大的連續內存區域。
  • 郭健:Linux內存管理系統參數配置之OOM(內存耗盡)
    運行參數的第二篇,主要是講OOM相關的參數的。在NUMA的情況下,有可能附加了其他的約束導致了系統遇到OOM狀態,實際上,系統中還有充足的內存。這些約束包括:(1)CONSTRAINT_CPUSET。cpusets是kernel中的一種機制,通過該機制可以把一組cpu和memory node資源分配給特定的一組進程。
  • Java包裝類入門
    我們都知道,JDK 其實給我們提供了很多很多 Java 開發者已經寫好的現成的類,他們其實都可以理解成工具類,比如我們常見的集合類,日期相關的類,數學相關的類等等,有了這些工具類,你會發現它能很大程度的幫你節省時間,能很方便的實現你的需求。當然,沒有這些包,你也能實現你的需求,但是你需要時間,今天我們主要是來學習一下包裝類。1、為什麼需要包裝類?
  • 線程上下文類加載器ContextClassLoader內存洩漏隱患
    前提在編寫Netty相關代碼的時候,從Netty源碼中的ThreadDeathWatcher和GlobalEventExecutor追溯到兩個和線程上下文類加載器ContextClassLoader內存洩漏相關的Issue:ThreadDeathWatcher
  • 【221期】面試官:談談內存洩漏和內存溢出的聯繫與區別
    來源:blog.csdn.net/leikun153/
  • 絕地求生大逃殺內存不足怎麼辦 絕地求生out of memory解決辦法
    絕地求生大逃殺內存不足怎麼辦 絕地求生out of memory解決辦法時間:2017-10-23 21:09   來源:今日頭條   責任編輯:毛青青 川北在線核心提示:原標題:絕地求生大逃殺內存不足怎麼辦 絕地求生out of memory解決辦法 《絕地求生大逃殺》的配置是很多玩家比較關心的問題,隨著遊戲的火爆程度越來越高
  • 【深入學習Redis】Redis內存模型
    在客戶端通過redis-cli連接伺服器後(後面如無特殊說明,客戶端一律使用redis-cli),通過info命令可以查看內存使用情況:其中,info命令可以顯示redis伺服器的許多信息,包括伺服器基本信息、CPU、內存、持久化、客戶端連接信息等等;memory是參數,表示只顯示內存相關的信息。