首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >多线程是否强调内存碎片?

多线程是否强调内存碎片?
EN

Stack Overflow用户
提问于 2011-05-03 21:33:41
回答 2查看 4.2K关注 0票数 40

描述

当使用openmp的并行构造分配和释放具有4个或更多线程的随机大小的内存块时,程序似乎开始在测试程序运行时的下半部分泄漏大量内存。因此,它将消耗的内存从1050 MB增加到1500 MB或更多,而不实际使用额外的内存。

由于valgrind没有显示任何问题,我必须假设,看起来是内存泄漏的实际上是内存碎片的强调效果。

有趣的是,如果两个线程每个分配10000次,那么效果还没有显示出来,但是如果4个线程每个分配了5000次,效果就会强烈地显示出来。另外,如果分配的块的最大大小减少到256 to (从1mb),效果就会变弱。

重并发能那么强调碎片化吗?或者这更有可能是堆中的一个bug?

测试程序描述

构建演示程序是为了从堆中获得256 MB随机大小的内存块,进行5000次分配。如果达到内存限制,首先分配的块将被解除分配,直到内存消耗低于该限制为止。一旦执行了5000次分配,就会释放所有内存并结束循环。所有这些工作都是针对openmp生成的每个线程完成的。

这种内存分配方案允许我们预期每个线程的内存消耗大约为260 MB (包括一些簿记数据)。

演示程序

由于这确实是您想要测试的东西,所以您可以从dropbox下载一个简单的makefile示例程序。

当按原样运行程序时,至少应该有1400 MB的RAM可用。可以随意调整代码中的常量以满足您的需要。

为了完整起见,实际代码如下:

代码语言:javascript
运行
复制
#include <stdlib.h>
#include <stdio.h>
#include <iostream>
#include <vector>
#include <deque>

#include <omp.h>
#include <math.h>

typedef unsigned long long uint64_t;

void runParallelAllocTest()
{
    // constants
    const int  NUM_ALLOCATIONS = 5000; // alloc's per thread
    const int  NUM_THREADS = 4;       // how many threads?
    const int  NUM_ITERS = NUM_THREADS;// how many overall repetions

    const bool USE_NEW      = true;   // use new or malloc? , seems to make no difference (as it should)
    const bool DEBUG_ALLOCS = false;  // debug output

    // pre store allocation sizes
    const int  NUM_PRE_ALLOCS = 20000;
    const uint64_t MEM_LIMIT = (1024 * 1024) * 256;   // x MB per process
    const size_t MAX_CHUNK_SIZE = 1024 * 1024 * 1;

    srand(1);
    std::vector<size_t> allocations;
    allocations.resize(NUM_PRE_ALLOCS);
    for (int i = 0; i < NUM_PRE_ALLOCS; i++) {
        allocations[i] = rand() % MAX_CHUNK_SIZE;   // use up to x MB chunks
    }


    #pragma omp parallel num_threads(NUM_THREADS)
    #pragma omp for
    for (int i = 0; i < NUM_ITERS; ++i) {
        uint64_t long totalAllocBytes = 0;
        uint64_t currAllocBytes = 0;

        std::deque< std::pair<char*, uint64_t> > pointers;
        const int myId = omp_get_thread_num();

        for (int j = 0; j < NUM_ALLOCATIONS; ++j) {
            // new allocation
            const size_t allocSize = allocations[(myId * 100 + j) % NUM_PRE_ALLOCS ];

            char* pnt = NULL;
            if (USE_NEW) {
                pnt = new char[allocSize];
            } else {
                pnt = (char*) malloc(allocSize);
            }
            pointers.push_back(std::make_pair(pnt, allocSize));

            totalAllocBytes += allocSize;
            currAllocBytes  += allocSize;

            // fill with values to add "delay"
            for (int fill = 0; fill < (int) allocSize; ++fill) {
                pnt[fill] = (char)(j % 255);
            }


            if (DEBUG_ALLOCS) {
                std::cout << "Id " << myId << " New alloc " << pointers.size() << ", bytes:" << allocSize << " at " << (uint64_t) pnt << "\n";
            }

            // free all or just a bit
            if (((j % 5) == 0) || (j == (NUM_ALLOCATIONS - 1))) {
                int frees = 0;

                // keep this much allocated
                // last check, free all
                uint64_t memLimit = MEM_LIMIT;
                if (j == NUM_ALLOCATIONS - 1) {
                    std::cout << "Id " << myId << " about to release all memory: " << (currAllocBytes / (double)(1024 * 1024)) << " MB" << std::endl;
                    memLimit = 0;
                }
                //MEM_LIMIT = 0; // DEBUG

                while (pointers.size() > 0 && (currAllocBytes > memLimit)) {
                    // free one of the first entries to allow previously obtained resources to 'live' longer
                    currAllocBytes -= pointers.front().second;
                    char* pnt       = pointers.front().first;

                    // free memory
                    if (USE_NEW) {
                        delete[] pnt;
                    } else {
                        free(pnt);
                    }

                    // update array
                    pointers.pop_front();

                    if (DEBUG_ALLOCS) {
                        std::cout << "Id " << myId << " Free'd " << pointers.size() << " at " << (uint64_t) pnt << "\n";
                    }
                    frees++;
                }
                if (DEBUG_ALLOCS) {
                    std::cout << "Frees " << frees << ", " << currAllocBytes << "/" << MEM_LIMIT << ", " << totalAllocBytes << "\n";
                }
            }
        } // for each allocation

        if (currAllocBytes != 0) {
            std::cerr << "Not all free'd!\n";
        }

        std::cout << "Id " << myId << " done, total alloc'ed " << ((double) totalAllocBytes / (double)(1024 * 1024)) << "MB \n";
    } // for each iteration

    exit(1);
}

int main(int argc, char** argv)
{
    runParallelAllocTest();

    return 0;
}

测试系统

据我所见,硬件非常重要。如果在更快的机器上运行,测试可能需要调整。

代码语言:javascript
运行
复制
Intel(R) Core(TM)2 Duo CPU     T7300  @ 2.00GHz
Ubuntu 10.04 LTS 64 bit
gcc 4.3, 4.4, 4.6
3988.62 Bogomips

测试

一旦执行了makefile,就应该得到一个名为ompmemtest的文件。为了查询一段时间的内存使用情况,我使用了以下命令:

代码语言:javascript
运行
复制
./ompmemtest &
top -b | grep ompmemtest

这就产生了相当令人印象深刻的碎裂或泄漏行为。4个线程的预期内存消耗是1090 MB,随着时间的推移,它变成了1500 MB:

代码语言:javascript
运行
复制
PID   USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
11626 byron     20   0  204m  99m 1000 R   27  2.5   0:00.81 ompmemtest                                                                              
11626 byron     20   0  992m 832m 1004 R  195 21.0   0:06.69 ompmemtest                                                                              
11626 byron     20   0 1118m 1.0g 1004 R  189 26.1   0:12.40 ompmemtest                                                                              
11626 byron     20   0 1218m 1.0g 1004 R  190 27.1   0:18.13 ompmemtest                                                                              
11626 byron     20   0 1282m 1.1g 1004 R  195 29.6   0:24.06 ompmemtest                                                                              
11626 byron     20   0 1471m 1.3g 1004 R  195 33.5   0:29.96 ompmemtest                                                                              
11626 byron     20   0 1469m 1.3g 1004 R  194 33.5   0:35.85 ompmemtest                                                                              
11626 byron     20   0 1469m 1.3g 1004 R  195 33.6   0:41.75 ompmemtest                                                                              
11626 byron     20   0 1636m 1.5g 1004 R  194 37.8   0:47.62 ompmemtest                                                                              
11626 byron     20   0 1660m 1.5g 1004 R  195 38.0   0:53.54 ompmemtest                                                                              
11626 byron     20   0 1669m 1.5g 1004 R  195 38.2   0:59.45 ompmemtest                                                                              
11626 byron     20   0 1664m 1.5g 1004 R  194 38.1   1:05.32 ompmemtest                                                                              
11626 byron     20   0 1724m 1.5g 1004 R  195 40.0   1:11.21 ompmemtest                                                                              
11626 byron     20   0 1724m 1.6g 1140 S  193 40.1   1:17.07 ompmemtest

请注意:I在编译gcc 4.3、4.4和4.6(主干)时可以复制这个问题。

EN

回答 2

Stack Overflow用户

发布于 2011-05-04 11:25:20

当将测试程序与google的tcmalloc库链接时,可执行文件不仅运行速度快了10%,而且显示出内存碎片大大减少或微不足道:

代码语言:javascript
运行
复制
PID   USER      PR  NI  VIRT  RES  SHR S %CPU %MEM    TIME+  COMMAND
13441 byron     20   0  379m 334m 1220 R  187  8.4   0:02.63 ompmemtestgoogle                                                                        
13441 byron     20   0 1085m 1.0g 1220 R  194 26.2   0:08.52 ompmemtestgoogle                                                                        
13441 byron     20   0 1111m 1.0g 1220 R  195 26.9   0:14.42 ompmemtestgoogle                                                                        
13441 byron     20   0 1131m 1.1g 1220 R  195 27.4   0:20.30 ompmemtestgoogle                                                                        
13441 byron     20   0 1137m 1.1g 1220 R  195 27.6   0:26.19 ompmemtestgoogle                                                                        
13441 byron     20   0 1137m 1.1g 1220 R  195 27.6   0:32.05 ompmemtestgoogle                                                                        
13441 byron     20   0 1149m 1.1g 1220 R  191 27.9   0:37.81 ompmemtestgoogle                                                                        
13441 byron     20   0 1149m 1.1g 1220 R  194 27.9   0:43.66 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1220 R  188 28.2   0:49.32 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1220 R  194 28.2   0:55.15 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1220 R  191 28.2   1:00.90 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1220 R  191 28.2   1:06.64 ompmemtestgoogle                                                                        
13441 byron     20   0 1161m 1.1g 1356 R  192 28.2   1:12.42 ompmemtestgoogle

根据我掌握的数据,答案似乎是:

如果所使用的堆库不能很好地处理并发访问,并且处理器不能真正同时执行线程,则多线程访问堆可以强调碎片化。

tcmalloc库显示没有显着的内存碎片,运行之前导致~400 in在碎片中丢失的程序。

但为什么会这样呢?

这里我必须提供的最好的想法是堆中的某种锁定工件。

测试程序将分配随机大小的内存块,释放程序早期分配的块以保持其内存限制。当一个线程正在释放“左”堆块中的旧内存时,它实际上可能会在另一个线程计划运行时停止,在该堆块上留下一个(软)锁。新调度的线程希望分配内存,但可能甚至不会读取“左侧”的堆块,以检查当前正在更改的空闲内存。因此,它可能会在“右边”不必要地使用一个新的堆块。

这个过程看起来像一个堆块移动,其中第一个块(左边)仍然只使用很少和支离破碎,迫使新的块被使用在右边。

让我们重申,只有当我在双核系统上使用4个或更多线程时,才会出现这种碎片问题,它只能或多或少地同时处理两个线程。当只使用两个线程时,堆上的(软)锁将保持得足够短,不会阻塞想要分配内存的其他线程。

另外,作为免责声明,我没有检查glibc堆实现的实际代码,也没有检查内存分配器领域的新手--我所写的只是它在我看来的样子,这使它成为纯粹的推测。

另一个有趣的读物可能是tcmalloc文档,它指出堆和多线程访问的常见问题,其中一些可能也在测试程序中扮演了他们的角色。

值得注意的是,它永远不会将内存返回到系统(请参阅http://goog-perftools.sourceforge.net/doc/tcmalloc.html中的注意事项段落)

票数 1
EN

Stack Overflow用户

发布于 2018-02-07 22:08:29

是的,默认的malloc (取决于linux版本)做了一些疯狂的事情,在一些多线程应用程序中大量失败。具体来说,它保持几乎每个线程堆(arenas),以避免锁定。对于所有线程来说,这比一个堆要快得多,但是大规模内存效率很低(有时)。您可以使用这样的代码对此进行调优,这会关闭多个竞技场(这会降低性能,因此,如果您有大量的小分配,就不要这样做!)

代码语言:javascript
运行
复制
rv = mallopt(-7, 1);  // M_ARENA_TEST
rv = mallopt(-8, 1);  // M_ARENA_MAX

或者就像其他人建议的那样,用各种替代物代替malloc。

基本上,一个通用的malloc不可能总是高效的,因为它不知道如何使用它。

ChrisP。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/5875989

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档