【问题标题】:Performance drop when adding memory that is not used添加未使用的内存时性能下降
【发布时间】:2020-11-28 16:34:36
【问题描述】:

当我偶然发现这种奇怪的性能下降时,我正在玩一个简单的“游戏”来测试面向数据设计的不同方面。

我有这个结构来存储游戏船的数据:

constexpr int MAX_ENEMY_SHIPS = 4000000;
struct Ships
{
    int32_t count;
    v2 pos[MAX_ENEMY_SHIPS];
    ShipMovement movements[MAX_ENEMY_SHIPS];
    ShipDrawing drawings[MAX_ENEMY_SHIPS];
    //ShipOtherData other[MAX_ENEMY_SHIPS]; 

    void Add(Ship ship)
    {
        pos[count] = ship.pos;
        movements[count] = { ship.dir, ship.speed };  
        drawings[count] = { ship.size, ship.color };
        //other[count] = { ship.a, ship.b, ship.c, ship.d };
        count++;
    }
};

然后我有一个更新运动数据的功能:

void MoveShips(v2* positions, ShipMovement* movements, int count)
{
    ScopeBenchmark bench("Move Ships");
    for(int i = 0; i < count; ++i)
    {
        positions[i] = positions[i] + (movements[i].dir * movements[i].speed);
    }
}

我的理解是,由于 MoveShips 函数仅使用位置和运动数组,因此 Ships 结构中的额外内存不会影响其性能。但是,当我取消注释 Ships 结构上的注释行时,性能会下降很多。使用当前的 MAX_ENEMY_SHIPS 值,我的计算机中 MoveShips 函数的持续时间从 10-11 毫秒变为 200-210 毫秒。

这里我举一个最小的、可重现的例子:

#include <stdlib.h>

#include <stdio.h>
#include <chrono>
#include <string>

class ScopeBenchmark
{
public:
    ScopeBenchmark(std::string text)
        : text(text)
    {
        begin = std::chrono::steady_clock::now();
    }

    ~ScopeBenchmark()
    {
        std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now();
        printf("%s: %lli\n", text.data(), std::chrono::duration_cast<std::chrono::milliseconds>(end - begin).count());
    }

private:
    std::string text;
    std::chrono::steady_clock::time_point begin;

};

constexpr int32_t Color(uint8_t r, uint8_t g, uint8_t b)
{
    return (r << 16) | (g << 8) | b;
}

struct v2
{
    float x;
    float y;
};

inline v2 operator+(v2 a, v2 b)
{
    v2 result;
    result.x = a.x + b.x;
    result.y = a.y + b.y;
    return result;
}

inline v2 operator*(v2 a, float b)
{
    v2 result;
    result.x = a.x * b;
    result.y = a.y * b;
    return result;
}

//----------------------------------------------------------------------

struct Ship
{
    v2 pos;
    v2 size;
    v2 dir;
    float speed;
    int32_t color;

    v2 a;
    v2 b;
    v2 c;
    v2 d;
};

struct ShipMovement
{
    v2 dir;
    float speed;
};

struct ShipDrawing
{
    v2 size;
    int32_t color;
};

struct ShipOtherData
{
    v2 a;
    v2 b;
    v2 c;
    v2 d;
};

constexpr int MAX_ENEMY_SHIPS = 4000000;
struct Ships
{
    int32_t count;
    v2 pos[MAX_ENEMY_SHIPS];
    ShipMovement movements[MAX_ENEMY_SHIPS];
    ShipDrawing drawings[MAX_ENEMY_SHIPS];
    //ShipOtherData other[MAX_ENEMY_SHIPS]; 

    void Add(Ship ship)
    {
        pos[count] = ship.pos;
        movements[count] = { ship.dir, ship.speed };  
        drawings[count] = { ship.size, ship.color };
        //other[count] = { ship.a, ship.b, ship.c, ship.d };
        count++;
    }
};

void MoveShips(v2* positions, ShipMovement* movements, int count)
{
    ScopeBenchmark bench("Move Ships");
    for(int i = 0; i < count; ++i)
    {
        positions[i] = positions[i] + (movements[i].dir * movements[i].speed);
    }
}

struct Game
{
    int32_t playerShipIndex;
    Ships ships;
};

void InitGame(void* gameMemory)
{
    Game* game = (Game*)gameMemory;
    
    Ship ship;
    ship.pos = { 0.0f, 0.0f };
    ship.size = { 100.0f, 100.0f };
    ship.speed = 1.0f;
    ship.color = Color(64, 192, 32);
    game->ships.Add(ship);
    game->playerShipIndex = 0;

    ship.speed *= 0.5f;
    ship.dir.x = -1.0f;
    ship.size = { 50.0f, 50.0f };
    ship.color = Color(192, 64, 32);

    for(int i = 0; i < MAX_ENEMY_SHIPS; i++)
    {
        ship.pos = { 500.0f, 350.0f };
        game->ships.Add(ship);
    }
}


int main()
{
    Game* game = (Game*)malloc(sizeof(Game));  
    memset(game, 0, sizeof(Game));
    
    InitGame(game);
    while (true)
    {
        MoveShips(game->ships.pos, game->ships.movements, game->ships.count);
    }
}

我使用 Visual Studio 编译器,并使用以下命令编译文件:

cl.exe /O2 /GL src/Game.cpp

那么,我的问题是:为什么在添加未使用的内存时,MoveShips 功能的性能下降如此之快?

【问题讨论】:

  • 这只是意味着分配一个大对象比分配一个小对象需要更多时间。
  • 嗯,它没有被使用,但仍然被分配。您要求 400 万个 8 字节的“东西”。使用或未使用,即每艘船 3200 万字节。对于一艘船,可行。对于一百或一千艘船,您开始谈论一些真实的记忆。以这种方式分配内存会使其远离其他往往会减慢速度的东西。然后,您必须将 3200 万次 Ships 的数量与系统上的可用内存进行比较。我猜你是在强迫你的系统进行大量交换,从而影响性能。
  • @Frank Merrow 为什么是 8 字节的东西?更有可能是 64 字节的......
  • 这很奇怪。当我在 MS Visual Studio 2017 上运行此代码作为发布版本时,有时我总是得到输出 11,有时我每次总是得到大约 500 的输出。无论是 11 还是 500 在每个程序运行中始终相同,但在程序的单独调用中可能会有所不同,即使我不重新编译也是如此。因此,这种极端差异与问题中提到的行是否被注释掉无关(在我的测试中,它们总是没有被注释掉)。
  • 使用std::vector

标签: c++ optimization data-oriented-design


【解决方案1】:

问题是您在函数调用game-&gt;ships.Add(ship) 中传递了未初始化的数据。这会导致undefined behavior

在第一个函数调用中,ship.dir.xship.dir.y 都未初始化。在所有进一步的函数调用中,ship.dir.y 未初始化。

如果ship.dir.y 恰好包含代表denormalized floating point value 的垃圾数据,这可能会对性能产生特别负面的影响。请参阅this question 了解更多信息。

我能够重现您的问题,并且我的测试表明这是您的性能下降的原因。通过将变量 ship.dir.y 初始化为标准化的浮点值,我能够可靠地将性能提高 45 倍(!)。

我认为您的问题与您通过取消注释两行代码来增加 struct 的大小没有任何关系。尽管在 cmets 部分中已经建议这可能会导致您的程序由于swap space 的使用而变慢,但我的测试表明这对您的情况没有显着的性能影响。将内存分配的总大小增加到 256 MB 通常不会有问题,除非您使用的是内存非常少的计算机。因此,我相信您观察到当您取消注释这两行代码时性能会显着下降只是一个巧合。

我的猜测是address space layout randomization (ASLR) 导致您每次运行程序时都会得到不同的垃圾值,因此它们有时代表非规范化的浮点值,有时不代表。至少这是我在测试期间所经历的:在激活 ASLR 的情况下,我有时会得到一个非规范化的值,有时会得到一个规范化的值。但是,在禁用 ALSR 的情况下(使用 MS Visual Studio 中的 /DYNAMICBASE:NO 链接器选项),我总是得到一个非规范化的值,而不是一个规范化的值。

如果您确定取消注释代码的观察结果并非巧合,而是一致的,那么最可能的解释是取消注释代码会导致您收到不同的垃圾值,而这些值恰好总是代表非规格化浮点值。

因此,为了解决您的问题,您所要做的就是确保 ship.dir.xship.dir.y 在传递给函数之前已正确初始化。

此外,虽然这可能不是您的问题的原因,但重要的是要指出您正在写入 struct Ships 中的所有 4 个数组超出范围。您正在调用函数game-&gt;ships.Add(ship) 准确MAX_ENEMY_SHIPS + 1 次,一次在循环外,MAX_ENEMY_SHIPS 次在循环内。因此,您将每个数组的边界恰好传递一个元素。这也会导致未定义的行为。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2013-05-10
    • 2014-05-27
    • 2020-02-05
    • 2011-11-07
    • 1970-01-01
    • 1970-01-01
    • 2014-03-02
    相关资源
    最近更新 更多