Content

关于NUMA

NUMA(Non-Uniform Memory Access)是一种内存架构,它允许多个处理器访问不同的物理内存区域,从而提高并行计算的性能。numa的主要优势是减少了内存访问的延迟和带宽限制,因为每个处理器可以优先使用自己附近的内存。很多现代处理器(例如Intel和AMD的多核处理器)都支持NUMA架构。
在NUMA架构中,每个处理器都会独立连接到一部分内存(称为本地内存),各个CPU之间则是通过QPI(Quick Path Interconnect)总线进行连接,CPU可以通过该总线访问其他CPU所辖内存(成为远程内存)。在该架构下,访问远程内存的时延会明显高于访问本地内存。因此,在追求性能的场景下就需要认真考虑如何合理分配内存以适应NUMA架构。

NUMA的配置

在Linux上,可以通过$ dmesg| grep -i numa命令检查系统是否支持NUMA。如果支持,可以通过$ sudo apt install numactl命令安装numactl工具。然后运行$ numactl --hardware来查看当前系统NUMA节点的状态。
此外,还可以通过numactl工具将程序绑定到指定的CPU核心或NUMA节点上。例如使用以下命令,将CPU0~15核心绑定到helloworld.out上:$ numactl -C 0-15 ./helloworld.out

NUMA的一些问题(陷阱)

  • 内存碎片化:由于不同的处理器可能会同时申请和释放内存,导致物理内存区域出现不连续的空闲块,这会降低内存利用率和分配效率。
  • 内存迁移:如果一个进程在运行过程中被调度到另一个处理器上,它可能会发现自己的内存分布在不同的物理区域上,这会增加内存访问的延迟和开销。
  • 内存亲和性:如果一个进程需要与其他进程或设备进行数据交换,它可能会希望将自己的内存分配在与之通信的对象相近的物理区域上,这样可以减少数据传输的时间和消耗。
    同理,一个进程中的多个线程如果分布在不同的NUMA节点上,并且需要访问远程内存,那么它们就可能会遇到远程访存的瓶颈。

为了解决这些问题,我们需要对numa下的内存分配进行优化。一种常用的方法是使用numa-aware的内存分配器,它可以根据进程的运行情况和需求,动态地调整内存分配策略,从而提高内存性能和效率。

  • 使用首次适应(first-fit)或最佳适应(best-fit)算法,尽量将内存分配在同一个物理区域上,避免内存碎片化。
  • 使用页迁移(page migration)或页复制(page replication)技术,根据进程的位置和活跃度,将其内存迁移到或复制到合适的物理区域上,减少内存迁移。
  • 使用亲和性掩码(affinity mask)或亲和性组(affinity group)机制,根据进程的通信模式和频率,将其内存分配在与之相关的对象相近的物理区域上,提高内存亲和性。

NUMA-aware

NUMA-aware指能够感知并利用NUMA架构特性的软件或程序(需要预先使用命令$ sudo apt-get install libnuma-dev安装libnunma-dev库)。如下的代码示例中,首先获取了NUMA节点的数量,然后为每个NUMA节点创建一个线程。每个线程中使用numa_run_on_node将线程绑定到指定的NUMA节点上,然后使用numa_alloc_local在本地内存中分配内存。由于各个线程被绑定到了不同的NUMA节点,线程均在各自节点上分配访问内存,程序性能大幅提升。

#include <iostream>
#include <thread>
#include <vector>
#include <numa.h>

int main() {
    // get the number of numa nodes
    int num_nodes = numa_num_configured_nodes();

    std::vector<std::thread> threads;
    for (int i = 0; i < num_nodes; i++) {
        threads.emplace_back([i] {
            // bind numa node
            numa_run_on_node(i);

            // alloc mem
            int *data = (int *)numa_alloc_local(1024 * sizeof(int));

            // deal with data
            for (int j = 0; j < 1024; j++) {
                data[j] = j;
            }

            // 释放内存
            numa_free(data, 1024 * sizeof(int));
        });
    }

    // wait for threads quit
    for (auto &t : threads) {
        t.join();
    }

    return 0;
}

补充

如果开启了NUMA,程序分配内存时就会优先分配本地内存,当本地内存空间不够时可以选择回收本地的PageCache(该策略一般会比较慢)或者是到其他NUMA上分配内存,可以通过Linux参数zone_reclaim_mode来配置,默认是到其他NUMA节点分配内存。

Zone_reclaim_mode allows someone to set more or less aggressive approaches to reclaim memory when a zone runs out of memory. If it is set to zero then no zone reclaim occurs. Allocations will be satisfied from other zones / nodes in the system.
0 = Allocate from all nodes before reclaiming memory
1 = Reclaim memory from local node vs allocating from next node
2 = Zone reclaim writes dirty pages out
3 = Zone reclaim swaps pages

参考资料

Last modification:September 6th, 2023 at 11:58 am
If you think my article is useful to you, please feel free to appreciate