記憶體管理#

緩衝區#

為了避免傳遞具有不同且不明顯生命週期規則的原始資料指標,Arrow 提供了一個稱為 arrow::Buffer 的通用抽象化。Buffer 封裝了指標和資料大小,通常也將其生命週期與底層提供者 (換句話說,Buffer 應始終指向有效的記憶體,直到其被銷毀)。緩衝區是無類型的:它們僅表示物理記憶體區域,而與其預期的含義或解釋無關。

緩衝區可以由 Arrow 本身配置,也可以由第三方常式配置。例如,可以將 Python 位元組字串的資料作為 Arrow 緩衝區傳遞,並在必要時保持 Python 物件的存活。

此外,緩衝區有多種形式:可變或不可變,可調整大小或不可調整大小。通常,您會在建立資料片段時持有可變緩衝區,然後將其凍結為不可變容器,例如陣列

注意

某些緩衝區可能指向非 CPU 記憶體,例如由 CUDA 環境提供的 GPU 後端記憶體。如果您正在編寫 GPU 感知的應用程式,則需要小心不要將 GPU 記憶體指標解釋為 CPU 可存取的指標,反之亦然。

存取緩衝區記憶體#

緩衝區使用 size()data() 存取器 (或 mutable_data() 用於可寫入存取可變緩衝區) 提供對底層記憶體的快速存取。

切片#

可以建立緩衝區的零複製切片,以取得指向底層資料的某些連續子集的緩衝區。這是透過呼叫 arrow::SliceBuffer()arrow::SliceMutableBuffer() 函式來完成的。

配置緩衝區#

您可以透過呼叫 arrow::AllocateBuffer()arrow::AllocateResizableBuffer() 多載之一來自行配置緩衝區

arrow::Result<std::unique_ptr<Buffer>> maybe_buffer = arrow::AllocateBuffer(4096);
if (!maybe_buffer.ok()) {
   // ... handle allocation error
}

std::shared_ptr<arrow::Buffer> buffer = *std::move(maybe_buffer);
uint8_t* buffer_data = buffer->mutable_data();
memcpy(buffer_data, "hello world", 11);

以這種方式配置緩衝區可確保其為 64 位元組對齊和填充,如Arrow 記憶體規格建議的那樣。

建立緩衝區#

您也可以使用 arrow::BufferBuilder API 逐步配置建立緩衝區

BufferBuilder builder;
builder.Resize(11);  // reserve enough space for 11 bytes
builder.Append("hello ", 6);
builder.Append("world", 5);

auto maybe_buffer = builder.Finish();
if (!maybe_buffer.ok()) {
   // ... handle buffer allocation error
}
std::shared_ptr<arrow::Buffer> buffer = *maybe_buffer;

如果緩衝區旨在包含給定固定寬度類型的值 (例如 List 陣列的 32 位元偏移量),則使用 arrow::TypedBufferBuilder 範本 API 可能更方便

TypedBufferBuilder<int32_t> builder;
builder.Reserve(2);  // reserve enough space for two int32_t values
builder.Append(0x12345678);
builder.Append(-0x765643210);

auto maybe_buffer = builder.Finish();
if (!maybe_buffer.ok()) {
   // ... handle buffer allocation error
}
std::shared_ptr<arrow::Buffer> buffer = *maybe_buffer;

記憶體池#

當使用 Arrow C++ API 配置緩衝區時,緩衝區的底層記憶體由 arrow::MemoryPool 實例配置。通常這會是程序範圍的預設記憶體池,但許多 Arrow API 允許您傳遞另一個 MemoryPool 實例以進行其內部配置。

記憶體池用於大型、長時間存在的資料,例如陣列緩衝區。其他資料,例如小型 C++ 物件和臨時工作區,通常會透過常規 C++ 配置器。

預設記憶體池#

預設記憶體池取決於 Arrow C++ 的編譯方式

  • 如果在編譯時啟用,則為 mimalloc 堆積;

  • 否則,如果在編譯時啟用,則為 jemalloc 堆積;

  • 否則,為 C 程式庫 malloc 堆積。

覆寫預設記憶體池#

可以透過設定 ARROW_DEFAULT_MEMORY_POOL 環境變數來覆寫上述選擇演算法。

STL 整合#

如果您希望使用 Arrow 記憶體池來配置 STL 容器的資料,可以使用 arrow::stl::allocator 包裝器來完成。

相反地,您也可以使用 STL 配置器來配置 Arrow 記憶體,使用 arrow::stl::STLMemoryPool 類別。但是,這效能可能較差,因為 STL 配置器不提供調整大小的操作。

裝置#

許多 Arrow 應用程式僅存取主機 (CPU) 記憶體。但是,在某些情況下,也希望處理裝置上記憶體(例如 GPU 上的板載記憶體)以及主機記憶體。

Arrow 使用 arrow::Device 抽象化來表示 CPU 和其他裝置。相關聯的類別 arrow::MemoryManager 指定如何在給定裝置上配置。每個裝置都有預設記憶體管理器,但可以建構其他實例 (例如,包裝自訂的 arrow::MemoryPool CPU)。arrow::MemoryManager 實例指定如何在給定裝置上配置記憶體 (例如,在 CPU 上使用特定的 arrow::MemoryPool )。

裝置無關程式設計#

如果您從第三方程式碼收到緩衝區,您可以透過呼叫其 is_cpu() 方法來查詢它是否可由 CPU 讀取。

您也可以透過呼叫 arrow::Buffer::View()arrow::Buffer::ViewOrCopy(),以通用方式在給定裝置上檢視緩衝區。如果來源和目標裝置相同,這將不會執行任何操作。否則,裝置相關機制將嘗試為目標裝置建構記憶體位址,以提供對緩衝區內容的存取權。實際的裝置到裝置傳輸可能會在讀取緩衝區內容時延遲發生。

同樣地,如果您想在緩衝區上執行 I/O 而不假設緩衝區可由 CPU 讀取,您可以呼叫 arrow::Buffer::GetReader()arrow::Buffer::GetWriter()

例如,若要取得任意緩衝區的 CPU 上檢視或副本,您可以簡單地執行

std::shared_ptr<arrow::Buffer> arbitrary_buffer = ... ;
std::shared_ptr<arrow::Buffer> cpu_buffer = arrow::Buffer::ViewOrCopy(
   arbitrary_buffer, arrow::default_cpu_memory_manager());

記憶體效能分析#

在 Linux 上,可以使用 perf record 產生記憶體配置的詳細設定檔,而無需修改二進位檔。這些設定檔除了配置大小外,還可以顯示追蹤記錄。這確實需要偵錯符號,來自偵錯組建或具有偵錯符號組建的版本。

注意

如果您要在另一個平台上分析 Arrow 的測試,可以使用 Archery 執行以下 Docker 容器以存取 Linux 環境

archery docker run ubuntu-cpp bash
# Inside the Docker container...
/arrow/ci/scripts/cpp_build.sh /arrow /build
cd build/cpp/debug
./arrow-array-test # Run a test
apt-get update
apt-get install -y linux-tools-generic
alias perf=/usr/lib/linux-tools/<version-path>/perf

若要追蹤配置,請在使用的每個配置器方法上建立探測點。收集 $params 可讓我們記錄請求的配置大小,而收集 $retval 可讓我們記錄已記錄配置的位址,以便我們可以將它們與 free/de-allocate 的呼叫相關聯。

perf probe -x libarrow.so je_arrow_mallocx '$params'
perf probe -x libarrow.so je_arrow_mallocx%return '$retval'
perf probe -x libarrow.so je_arrow_rallocx '$params'
perf probe -x libarrow.so je_arrow_rallocx%return '$retval'
perf probe -x libarrow.so je_arrow_dallocx '$params'
PROBE_ARGS="-e probe_libarrow:je_arrow_mallocx \
   -e probe_libarrow:je_arrow_mallocx__return \
   -e probe_libarrow:je_arrow_rallocx \
   -e probe_libarrow:je_arrow_rallocx__return \
   -e probe_libarrow:je_arrow_dallocx"
perf probe -x libarrow.so mi_malloc_aligned '$params'
perf probe -x libarrow.so mi_malloc_aligned%return '$retval'
perf probe -x libarrow.so mi_realloc_aligned '$params'
perf probe -x libarrow.so mi_realloc_aligned%return '$retval'
perf probe -x libarrow.so mi_free '$params'
PROBE_ARGS="-e probe_libarrow:mi_malloc_aligned \
   -e probe_libarrow:mi_malloc_aligned__return \
   -e probe_libarrow:mi_realloc_aligned \
   -e probe_libarrow:mi_realloc_aligned__return \
   -e probe_libarrow:mi_free"

設定探測後,您可以使用 perf record 記錄具有相關追蹤記錄的呼叫。在本範例中,我們正在 Arrow 中執行 StructArray 單元測試

perf record -g --call-graph dwarf \
  $PROBE_ARGS \
  ./arrow-array-test --gtest_filter=StructArray*

如果您想分析正在執行的程序,可以執行 perf record -p <PID>,它將記錄直到您使用 CTRL+C 中斷。或者,您可以執行 perf record -P <PID> sleep 10 以記錄 10 秒。

產生的資料可以使用標準工具處理以使用 perf,或者可以使用 perf script 將資料的文字格式管道傳輸到自訂腳本。以下腳本剖析 perf script 輸出,並以換行符號分隔的 JSON 格式列印輸出,以便於處理。

process_perf_events.py#
import sys
import re
import json

# Example non-traceback line
# arrow-array-tes 14344 [003]  7501.073802: probe_libarrow:je_arrow_mallocx: (7fbcd20bb640) size=0x80 flags=6

current = {}
current_traceback = ''

def new_row():
    global current_traceback
    current['traceback'] = current_traceback
    print(json.dumps(current))
    current_traceback = ''

for line in sys.stdin:
    if line == '\n':
        continue
    elif line[0] == '\t':
        # traceback line
        current_traceback += line.strip("\t")
    else:
        line = line.rstrip('\n')
        if not len(current) == 0:
            new_row()
        parts = re.sub(' +', ' ', line).split(' ')

        parts.reverse()
        parts.pop() # file
        parts.pop() # "14344"
        parts.pop() # "[003]"

        current['time'] = float(parts.pop().rstrip(":"))
        current['event'] = parts.pop().rstrip(":")

        parts.pop() # (7fbcd20bddf0)
        if parts[-1] == "<-":
            parts.pop()
            parts.pop()

        params = {}

        for pair in parts:
            key, value = pair.split("=")
            params[key] = value

        current['params'] = params

以下是該腳本的範例調用,以及輸出資料的預覽

$ perf script | python3 /arrow/process_perf_events.py > processed_events.jsonl
$ head processed_events.jsonl | cut -c -120
{"time": 14814.954378, "event": "probe_libarrow:je_arrow_mallocx", "params": {"flags": "6", "size": "0x80"}, "traceback"
{"time": 14814.95443, "event": "probe_libarrow:je_arrow_mallocx__return", "params": {"arg1": "0x7f4a97e09000"}, "traceba
{"time": 14814.95448, "event": "probe_libarrow:je_arrow_mallocx", "params": {"flags": "6", "size": "0x40"}, "traceback":
{"time": 14814.954486, "event": "probe_libarrow:je_arrow_mallocx__return", "params": {"arg1": "0x7f4a97e0a000"}, "traceb
{"time": 14814.954502, "event": "probe_libarrow:je_arrow_rallocx", "params": {"flags": "6", "size": "0x40", "ptr": "0x7f
{"time": 14814.954507, "event": "probe_libarrow:je_arrow_rallocx__return", "params": {"arg1": "0x7f4a97e0a040"}, "traceb
{"time": 14814.954796, "event": "probe_libarrow:je_arrow_mallocx", "params": {"flags": "6", "size": "0x40"}, "traceback"
{"time": 14814.954805, "event": "probe_libarrow:je_arrow_mallocx__return", "params": {"arg1": "0x7f4a97e0a080"}, "traceb
{"time": 14814.954817, "event": "probe_libarrow:je_arrow_mallocx", "params": {"flags": "6", "size": "0x40"}, "traceback"
{"time": 14814.95482, "event": "probe_libarrow:je_arrow_mallocx__return", "params": {"arg1": "0x7f4a97e0a0c0"}, "traceba

從那裡可以回答許多問題。例如,以下腳本將找到哪些配置從未釋放,並印出相關的追蹤記錄以及懸空配置的計數

count_tracebacks.py#
'''Find tracebacks of allocations with no corresponding free'''
import sys
import json
from collections import defaultdict

allocated = dict()

for line in sys.stdin:
    line = line.rstrip('\n')
    data = json.loads(line)

    if data['event'] == "probe_libarrow:je_arrow_mallocx__return":
        address = data['params']['arg1']
        allocated[address] = data['traceback']
    elif data['event'] == "probe_libarrow:je_arrow_rallocx":
        address = data['params']['ptr']
        del allocated[address]
    elif data['event'] == "probe_libarrow:je_arrow_rallocx__return":
        address = data['params']['arg1']
        allocated[address] = data['traceback']
    elif data['event'] == "probe_libarrow:je_arrow_dallocx":
        address = data['params']['ptr']
        if address in allocated:
            del allocated[address]
    elif data['event'] == "probe_libarrow:mi_malloc_aligned__return":
        address = data['params']['arg1']
        allocated[address] = data['traceback']
    elif data['event'] == "probe_libarrow:mi_realloc_aligned":
        address = data['params']['p']
        del allocated[address]
    elif data['event'] == "probe_libarrow:mi_realloc_aligned__return":
        address = data['params']['arg1']
        allocated[address] = data['traceback']
    elif data['event'] == "probe_libarrow:mi_free":
        address = data['params']['p']
        if address in allocated:
            del allocated[address]

traceback_counts = defaultdict(int)

for traceback in allocated.values():
    traceback_counts[traceback] += 1

for traceback, count in sorted(traceback_counts.items(), key=lambda x: -x[1]):
    print("Num of dangling allocations:", count)
    print(traceback)

可以像這樣調用腳本

$ cat processed_events.jsonl | python3 /arrow/count_tracebacks.py
Num of dangling allocations: 1
 7fc945e5cfd2 arrow::(anonymous namespace)::JemallocAllocator::ReallocateAligned+0x13b (/build/cpp/debug/libarrow.so.700.0.0)
 7fc945e5fe4f arrow::BaseMemoryPoolImpl<arrow::(anonymous namespace)::JemallocAllocator>::Reallocate+0x93 (/build/cpp/debug/libarrow.so.700.0.0)
 7fc945e618f7 arrow::PoolBuffer::Resize+0xed (/build/cpp/debug/libarrow.so.700.0.0)
 55a38b163859 arrow::BufferBuilder::Resize+0x12d (/build/cpp/debug/arrow-array-test)
 55a38b163bbe arrow::BufferBuilder::Finish+0x48 (/build/cpp/debug/arrow-array-test)
 55a38b163e3a arrow::BufferBuilder::Finish+0x50 (/build/cpp/debug/arrow-array-test)
 55a38b163f90 arrow::BufferBuilder::FinishWithLength+0x4e (/build/cpp/debug/arrow-array-test)
 55a38b2c8fa7 arrow::TypedBufferBuilder<int, void>::FinishWithLength+0x4f (/build/cpp/debug/arrow-array-test)
 55a38b2bcce7 arrow::NumericBuilder<arrow::Int32Type>::FinishInternal+0x107 (/build/cpp/debug/arrow-array-test)
 7fc945c065ae arrow::ArrayBuilder::Finish+0x5a (/build/cpp/debug/libarrow.so.700.0.0)
 7fc94736ed41 arrow::ipc::internal::json::(anonymous namespace)::Converter::Finish+0x123 (/build/cpp/debug/libarrow.so.700.0.0)
 7fc94737426e arrow::ipc::internal::json::ArrayFromJSON+0x299 (/build/cpp/debug/libarrow.so.700.0.0)
 7fc948e98858 arrow::ArrayFromJSON+0x64 (/build/cpp/debug/libarrow_testing.so.700.0.0)
 55a38b6773f3 arrow::StructArray_FlattenOfSlice_Test::TestBody+0x79 (/build/cpp/debug/arrow-array-test)
 7fc944689633 testing::internal::HandleSehExceptionsInMethodIfSupported<testing::Test, void>+0x68 (/build/cpp/googletest_ep-prefix/lib/libgtestd.so.1.11.0)
 7fc94468132a testing::internal::HandleExceptionsInMethodIfSupported<testing::Test, void>+0x5d (/build/cpp/googletest_ep-prefix/lib/libgtestd.so.1.11.0)
 7fc9446555eb testing::Test::Run+0xf1 (/build/cpp/googletest_ep-prefix/lib/libgtestd.so.1.11.0)
 7fc94465602d testing::TestInfo::Run+0x13f (/build/cpp/googletest_ep-prefix/lib/libgtestd.so.1.11.0)
 7fc944656947 testing::TestSuite::Run+0x14b (/build/cpp/googletest_ep-prefix/lib/libgtestd.so.1.11.0)
 7fc9446663f5 testing::internal::UnitTestImpl::RunAllTests+0x433 (/build/cpp/googletest_ep-prefix/lib/libgtestd.so.1.11.0)
 7fc94468ab61 testing::internal::HandleSehExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>+0x68 (/build/cpp/googletest_ep-prefix/lib/libgtestd.so.1.11.0)
 7fc944682568 testing::internal::HandleExceptionsInMethodIfSupported<testing::internal::UnitTestImpl, bool>+0x5d (/build/cpp/googletest_ep-prefix/lib/libgtestd.so.1.11.0)
 7fc944664b0c testing::UnitTest::Run+0xcc (/build/cpp/googletest_ep-prefix/lib/libgtestd.so.1.11.0)
 7fc9446d0299 RUN_ALL_TESTS+0x14 (/build/cpp/googletest_ep-prefix/lib/libgtest_maind.so.1.11.0)
 7fc9446d021b main+0x42 (/build/cpp/googletest_ep-prefix/lib/libgtest_maind.so.1.11.0)
 7fc9441e70b2 __libc_start_main+0xf2 (/usr/lib/x86_64-linux-gnu/libc-2.31.so)
 55a38b10a50d _start+0x2d (/build/cpp/debug/arrow-array-test)