C语言——统一内存分配

简介

统一内存分配(Unified Memory Allocation,UMA)是一种内存管理技术,它的目标是通过减少内存分配次数、提高内存的使用效率以及保证内存的连续性来提升性能。

优势

  • (1) 减少内存分配次数,降低性能开销;
  • (2) 内存的连续性,提高数据访问效率和缓存一致性;
  • (3) 简化内存管理,减少内存泄漏和错误;
  • (4) 提高并发性能,减少锁竞争;

(1) 在程序中频繁地进行内存分配和释放(尤其是动态分配)会导致严重的性能问题,主要表现为:

  • 内存碎片化:频繁分配和释放内存块可能导致内存碎片化,从而降低内存的利用率。
  • 内存分配和释放的开销:内存分配和释放通常会涉及操作系统的管理,尤其是在复杂的内存管理系统中,可能需要进行锁操作、搜索空闲内存块等,增加了系统的开销。

(2) 统一内存分配技术通常会分配一块 连续的内存区域,这对性能的提升有很大帮助,具体体现在以下几个方面:

  • 提高数据访问效率:在现代计算机架构中,CPU 和内存之间的访问效率与数据的布局密切相关。如果程序的数据是连续存储的,那么 CPU 可以更高效地进行内存访问,因为数据很可能一次就能够被加载到缓存中,减少了缓存未命中的概率。
  • 增强缓存一致性:通过将内存分配集中到一个大块区域,可以提高数据访问的局部性,从而增强 CPU 缓存的效率。因为在同一个区域中的数据通常更接近,访问效率会比分散在不同区域的数据更高。
  • 减少内存访问延迟:分散的内存区域可能会导致多个缓存行的访问,增加了内存访问的延迟。而连续的内存区域能提高空间局部性,使得访问延迟降低。
  • 提高硬件效率:一些硬件(如图形处理单元 GPU)在访问内存时也可能更高效,特别是当内存是连续的时。GPU 等并行计算设备在处理大量数据时,对内存访问的连续性要求较高,能直接提高计算吞吐量。

(3) 传统的内存分配往往需要开发者手动处理分配、释放和重用内存,容易出现内存泄漏、野指针等问题。统一内存分配通过集中管理内存池和内存块,简化了内存的分配和释放过程,从而减少了人为错误和维护成本。

  • 统一内存池:统一内存池管理机制减少了分配、释放和合并内存的复杂性。开发者只需要通过池来申请和释放内存,而不需要关心内存的具体管理细节。
  • 内存池的生命周期管理:内存池通常可以一次性释放或重置,而不是逐个释放内存块。这减少了内存泄漏的风险,并且可以提高系统的稳定性。

(4) 在并发系统中,内存分配的性能可能会成为瓶颈。传统的内存分配策略(尤其是在多线程环境中)会出现锁竞争,导致性能下降。统一内存分配技术通过集中管理内存,可以减少竞争,提供更好的并发性能。

  • 减少锁的使用:由于内存池内存分配已经集中管理,多个线程可以更高效地共享同一内存池,从而减少了对内存管理锁的需求。
  • 内存分配的局部性:内存池中的内存块通常按照一定的模式进行分配(例如,每个线程独占一定的内存区域),从而减少了线程间的竞争,进一步提高了多线程并发下的性能。

简单示例(相同数据类型)

使用一次 malloc 来分配多个内存区域可以有效减少分配的次数,从而减少性能开销。

在这种情况下,首先你需要计算出需要的总内存大小,然后通过一次 malloc 分配出足够的内存,并使用指针偏移的方式来访问每个内存区域。

#include <stdio.h>
#include <stdlib.h>

int main() {
    // 一次分配60字节的内存
    char *pA = malloc(60); // 分配60字节内存

    // 检查是否分配成功
    if (pA == NULL) {
        printf("Memory allocation failed!\n");
        return 1;
    }

    // 按照需要的偏移量,设置pB和pC
    char *pB = pA + 10;  // pB指向第10个字节
    char *pC = pB + 20;  // pC指向第30个字节

    // 使用pA, pB, pC进行操作
    pA[0] = 'A'; pB[0] = 'B'; pC[0] = 'C';
    
    // 输出检查
    printf("pA[0]: %c, pB[0]: %c, pC[0]: %c\n", pA[0], pB[0], pC[0]);

    // 释放内存
    free(pA);

    return 0;
}

pA 指向分配的内存区域的起始位置,pB 和 pC 是通过指针算术访问不同的内存部分。这样做是安全的,只要确保你不会越界访问分配的内存。

  • pA 指向第一个区域(10字节)。
  • pB 指向第二个区域(20字节),它是 pA 加上 10 字节的偏移。
  • pC 指向第三个区域(30字节),它是 pB 加上 20 字节的偏移。

在释放内存时,你只能释放 malloc 返回的原始指针,即 pA,而不能尝试释放 pB 或 pC,因为它们只是通过偏移得到的指针。内存释放必须通过原始的分配指针进行,而不能通过偏移后的指针来释放。

free(pA); // 释放最初的内存分配

这种方法在性能上是有优势的,因为只进行了一次 malloc 分配,可以避免多次分配带来的开销。但要注意内存管理的细节,确保指针正确且没有越界。

简单示例(不同数据类型)

#include <stdio.h>
#include <stdlib.h>

typedef struct {
    int x;
    int y;
} Point;

typedef struct {
    char name[50];
    int age;
} Person;

typedef struct {
    float balance;
    int account_number;
} Account;

int main() {
    // 计算总的内存大小
    size_t total_size = sizeof(Point) + sizeof(Person) + sizeof(Account);
    
    // 一次性分配足够大的内存
    char *pAll = malloc(total_size);

    if (pAll == NULL) {
        printf("内存分配失败\n");
        return 1;
    }

    // 使用偏移量来访问每个结构体
    Point *pPoint = (Point *)pAll;                        // pPoint 指向第一个结构体(Point)
    Person *pPerson = (Person *)(pAll + sizeof(Point));   // pPerson 指向第二个结构体(Person)
    Account *pAccount = (Account *)(pAll + sizeof(Point) + sizeof(Person)); // pAccount 指向第三个结构体(Account)

    // 为各个结构体的字段赋值
    pPoint->x = 10;
    pPoint->y = 20;

    snprintf(pPerson->name, sizeof(pPerson->name), "Alice");
    pPerson->age = 30;

    pAccount->balance = 1000.50;
    pAccount->account_number = 123456789;

    // 打印每个结构体的内容
    printf("Point: (%d, %d)\n", pPoint->x, pPoint->y);
    printf("Person: %s, Age: %d\n", pPerson->name, pPerson->age);
    printf("Account Balance: %.2f, Account Number: %d\n", pAccount->balance, pAccount->account_number);

    // 释放内存
    free(pAll);

    return 0;
}

注意事项:

  • 内存对齐:结构体中的成员可能会进行内存对齐,因此每个结构体的内存大小可能大于它的成员大小的总和。如果你需要完全按结构体大小计算内存,可以直接使用 sizeof 来获得实际的内存大小。
  • 结构体顺序和偏移量:结构体之间的顺序和大小很重要,确保按照正确的顺序使用偏移量来访问不同的结构体。
posted @ 2025-01-21 16:18  岸南  阅读(108)  评论(0)    收藏  举报