统计数据系统概述

统计数据系统 可以收集和显示性能数据,从而用来优化游戏。

要获取关于统计命令的帮助,请在控制台中输入 stat,或参阅 PrintStatsHelpToOutputDevice(); 方法。

类型

统计数据系统支持下列类型:

统计数据类型 说明
循环计数器 一种泛型循环计数器,用于统计数据对象生命周期中的循环次数。
浮点/Dword计数器 一种每帧都会清空的计数器。
浮点/Dword累加器 一种不会每帧清空的计数器,作为可重置的持久统计数据。
内存 一种特殊类型的计数器,针对内存跟踪进行优化。

分组统计数据

每个统计数据必须归入组中,通常对应显示指定的统计数据组。例如,stat statsystem 将显示统计数据相关数据。

要定义统计数据组,请使用下列方法之一:

方法 说明
DECLARE_STATS_GROUP(GroupDesc, GroupId, GroupCat) 声明默认启用的统计数据组。
DECLARE_STATS_GROUP_VERBOSE(GroupDesc, GroupId, GroupCat) 声明默认禁用的统计数据组。
DECLARE_STATS_GROUP_MAYBE_COMPILED_OUT(GroupDesc, GroupId, GroupCat) 声明默认禁用的统计数据组,编译器可将该组剥离。

其中:

  • GroupDesc 是该组的文本描述。
  • GroupId 是该组的 独有 标识
  • GroupCat 保留供将来使用
  • CompileIn 如设为true,编译器则可能将其剥离出来

根据作用域,可在源文件或标头文件中完成分组。

示例用法

    DECLARE_STATS_GROUP(TEXT("Threading"), STATGROUP_Threading, STATCAT_Advanced);
    DECLARE_STATS_GROUP_VERBOSE(TEXT("Linker Load"), STATGROUP_LinkerLoad, STATCAT_Advanced);

Copy full snippet

DECLARE_STATS_GROUP(TEXT("Threading"), STATGROUP_Threading, STATCAT_Advanced); DECLARE_STATS_GROUP_VERBOSE(TEXT("Linker Load"), STATGROUP_LinkerLoad, STATCAT_Advanced);

声明和定义统计数据

现在可声明和定义统计数据,但在此之前请注意,统计数据可用在:

  • 仅一个cpp文件
  • 函数作用域
  • 模块作用域
  • 整个项目

用于单个文件

如作用域是单个文件,必须根据统计数据类型使用下列一种方法:

方法 说明
DECLARE_CYCLE_STAT(CounterName, StatId, GroupId) 声明循环计数器统计数据。
DECLARE_SCOPE_CYCLE_COUNTER(CounterName, StatId, GroupId) 声明循环计数器统计数据,同时使用它。此外,它仅限于一个函数作用域。
QUICK_SCOPE_CYCLE_COUNTER(StatId) 声明循环计数器统计数据,它将属于名为"Quick"的统计数据组。
RETURN_QUICK_DECLARE_CYCLE_STAT(StatId, GroupId) 返回循环计数器,有时由几个专门的类使用。
DECLARE_FLOAT_COUNTER_STAT(CounterName, StatId, GroupId) 声明浮点计数器,基于双倍类型(8字节)。
DECLARE_DWORD_COUNTER_STAT(CounterName, StatId, GroupId) 声明dword计数器,基于qword类型(8字节)。
DECLARE_FLOAT_ACCUMULATOR_STAT(CounterName, StatId, GroupId) 声明浮点累加器。
DECLARE_DWORD_ACCUMULATOR_STAT(CounterName, StatId, GroupId) 声明dword累加器。
DECLARE_MEMORY_STAT(CounterName, StatId, GroupId) 声明与dword累加器相同的内存计数器,但将使用内存特定单位显示。
DECLARE_MEMORY_STAT_POOL(CounterName, StatId, GroupId, Pool) 声明带有池的内存计数器。

用于多个文件

如想拥有可供整个项目(或范围较广的文件)访问的统计数据,需使用其外部版本。 这些方法与先前提到的方法相同,但名称末尾带有 _EXTERN:

DECLARE_CYCLE_STAT_EXTERN(CounterName, StatId, GroupId, API) DECLARE_FLOAT_COUNTER_STAT_EXTERN(CounterName, StatId, GroupId, API) DECLARE_DWORD_COUNTER_STAT_EXTERN(CounterName, StatId, GroupId, API) DECLARE_FLOAT_ACCUMULATOR_STAT_EXTERN(CounterName, StatId, GroupId, API) DECLARE_DWORD_ACCUMULATOR_STAT_EXTERN(CounterName, StatId, GroupId, API) DECLARE_MEMORY_STAT_EXTERN(CounterName, StatId, GroupId, API) DECLARE_MEMORY_STAT_POOL_EXTERN(CounterName, StatId, GroupId, Pool, API)

然后在源文件中,需使用下列项定义这些统计数据,这些项定义的以 _EXTERN 声明的统计数据:

其中:

  • CounterName 是统计数据的文本描述
  • StatId 是统计数据的 独有 标识
  • GroupId 是统计数据所属的组的标识, GroupId来自DECLARE_STATS_GROUP*`
  • Pool 是平台专属的内存池
  • API 是模块的 *_API,如该统计数据仅使用在该模块中,其可为空

示例

带有池的自定义内存统计数据

首先需添加新池到 enum EMemoryCounterRegion,它可以是全局的,也可以是平台专属的:

    enum EMemoryCounterRegion
    {
        MCR_Invalid,        // 非内存
        MCR_Physical,       // 主系统内存
        MCR_GPU,            // GPU内存(显卡)
        MCR_GPUSystem,      // GPU直接访问的系统内存
        MCR_TexturePool,    // 预设置大小的纹理池
        MCR_MAX
    };

Copy full snippet

enum EMemoryCounterRegion { MCR_Invalid, // 非内存 MCR_Physical, // 主系统内存 MCR_GPU, // GPU内存(显卡) MCR_GPUSystem, // GPU直接访问的系统内存 MCR_TexturePool, // 预设置大小的纹理池 MCR_MAX };

下面是允许在任何地方使用池的示例(见 CORE_API)。

池名称必须以 MCR_ 开头。

标头文件范例

    DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Physical Memory Pool [Physical]"), MCR_Physical, STATGROUP_Memory, FPlatformMemory::MCR_Physical, CORE_API);
    DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("GPU Memory Pool [GPU]"), MCR_GPU,STATGROUP_Memory, FPlatformMemory::MCR_GPU, CORE_API);
    DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Texture Memory Pool [Texture]"), MCR_TexturePool, STATGROUP_Memory, FPlatformMemory::MCR_TexturePool,CORE_API);

Copy full snippet

DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Physical Memory Pool [Physical]"), MCR_Physical, STATGROUP_Memory, FPlatformMemory::MCR_Physical, CORE_API); DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("GPU Memory Pool [GPU]"), MCR_GPU,STATGROUP_Memory, FPlatformMemory::MCR_GPU, CORE_API); DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Texture Memory Pool [Texture]"), MCR_TexturePool, STATGROUP_Memory, FPlatformMemory::MCR_TexturePool,CORE_API);

源文件范例

    DEFINE_STAT(MCR_Physical);
    DEFINE_STAT(MCR_GPU);
    DEFINE_STAT(MCR_TexturePool);

    // 这是一个池,因此需要初始化——通常在F*PlatformMemory::Init()之中。
    SET_MEMORY_STAT(MCR_Physical, PhysicalPoolLimit);
    SET_MEMORY_STAT(MCR_GPU, GPUPoolLimit);
    SET_MEMORY_STAT(MCR_TexturePool, TexturePoolLimit);

    // 拥有池之后,即可为其设置内存统计数据。

    // 以下内容可在任意处访问。
    DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Index buffer memory"), STAT_IndexBufferMemory, STATGROUP_RHI, FPlatformMemory::MCR_GPU, RHI_API);
    DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Vertex buffer memory"), STAT_VertexBufferMemory, STATGROUP_RHI, FPlatformMemory::MCR_GPU, RHI_API);
    DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Structured buffer memory"), STAT_StructuredBufferMemory,STATGROUP_RHI, FPlatformMemory::MCR_GPU, RHI_API);
    DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Pixel buffer memory"), STAT_PixelBufferMemory, STATGROUP_RHI, FPlatformMemory::MCR_GPU, RHI_API);

    // 以下内容只能在其被定义的模块中访问。
    DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Pool Memory Size"), STAT_TexturePoolSize, STATGROUP_Streaming, FPlatformMemory::MCR_TexturePool, );
    DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Pool Memory Used"), STAT_TexturePoolAllocatedSize, STATGROUP_Streaming, FPlatformMemory::MCR_TexturePool, );

    // 最后,我们需要更新内存统计数据。

    // 以特定的值增加内存统计数据。
    INC_MEMORY_STAT_BY(STAT_PixelBufferMemory,NumBytes);
    // 以特定的值减少内存统计数据。
    DEC_MEMORY_STAT_BY(STAT_PixelBufferMemory,NumBytes);
    // 以特定的值设置内存统计数据。
    SET_MEMORY_STAT(STAT_PixelBufferMemory,NumBytes);

Copy full snippet

DEFINE_STAT(MCR_Physical); DEFINE_STAT(MCR_GPU); DEFINE_STAT(MCR_TexturePool);

// 这是一个池,因此需要初始化——通常在F*PlatformMemory::Init()之中。
SET_MEMORY_STAT(MCR_Physical, PhysicalPoolLimit);
SET_MEMORY_STAT(MCR_GPU, GPUPoolLimit);
SET_MEMORY_STAT(MCR_TexturePool, TexturePoolLimit);

// 拥有池之后,即可为其设置内存统计数据。

// 以下内容可在任意处访问。
DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Index buffer memory"), STAT_IndexBufferMemory, STATGROUP_RHI, FPlatformMemory::MCR_GPU, RHI_API);
DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Vertex buffer memory"), STAT_VertexBufferMemory, STATGROUP_RHI, FPlatformMemory::MCR_GPU, RHI_API);
DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Structured buffer memory"), STAT_StructuredBufferMemory,STATGROUP_RHI, FPlatformMemory::MCR_GPU, RHI_API);
DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Pixel buffer memory"), STAT_PixelBufferMemory, STATGROUP_RHI, FPlatformMemory::MCR_GPU, RHI_API);

// 以下内容只能在其被定义的模块中访问。
DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Pool Memory Size"), STAT_TexturePoolSize, STATGROUP_Streaming, FPlatformMemory::MCR_TexturePool, );
DECLARE_MEMORY_STAT_POOL_EXTERN(TEXT("Pool Memory Used"), STAT_TexturePoolAllocatedSize, STATGROUP_Streaming, FPlatformMemory::MCR_TexturePool, );

// 最后,我们需要更新内存统计数据。

// 以特定的值增加内存统计数据。
INC_MEMORY_STAT_BY(STAT_PixelBufferMemory,NumBytes);
// 以特定的值减少内存统计数据。
DEC_MEMORY_STAT_BY(STAT_PixelBufferMemory,NumBytes);
// 以特定的值设置内存统计数据。
SET_MEMORY_STAT(STAT_PixelBufferMemory,NumBytes);

不带池的常规内存统计数据

    DECLARE_MEMORY_STAT(TEXT("Total Physical"), STAT_TotalPhysical, STATGROUP_MemoryPlatform);
    DECLARE_MEMORY_STAT(TEXT("Total Virtual"), STAT_TotalVirtual, STATGROUP_MemoryPlatform);
    DECLARE_MEMORY_STAT(TEXT("Page Size"), STAT_PageSize, STATGROUP_MemoryPlatform);
    DECLARE_MEMORY_STAT(TEXT("Total Physical GB"), STAT_TotalPhysicalGB, STATGROUP_MemoryPlatform);

Copy full snippet

DECLARE_MEMORY_STAT(TEXT("Total Physical"), STAT_TotalPhysical, STATGROUP_MemoryPlatform); DECLARE_MEMORY_STAT(TEXT("Total Virtual"), STAT_TotalVirtual, STATGROUP_MemoryPlatform); DECLARE_MEMORY_STAT(TEXT("Page Size"), STAT_PageSize, STATGROUP_MemoryPlatform); DECLARE_MEMORY_STAT(TEXT("Total Physical GB"), STAT_TotalPhysicalGB, STATGROUP_MemoryPlatform);

或者,如果愿意,可在标头文件中 DECLARE_MEMORY_STAT_EXTERN,然后在源文件中 DEFINE_STAT。

更新内存统计数据的方式与带有池的版本相同。

使用循环计数器的性能数据

首先需添加循环计数器:

    DECLARE_CYCLE_STAT(TEXT("Broadcast"), STAT_StatsBroadcast, STATGROUP_StatSystem);
    DECLARE_CYCLE_STAT(TEXT("Condense"), STAT_StatsCondense, STATGROUP_StatSystem);

Copy full snippet

DECLARE_CYCLE_STAT(TEXT("Broadcast"), STAT_StatsBroadcast, STATGROUP_StatSystem); DECLARE_CYCLE_STAT(TEXT("Condense"), STAT_StatsCondense, STATGROUP_StatSystem);

或在标头文件中 DECLARE_CYCLE_STAT_EXTERN,然后在源文件中 DEFINE_STAT。

现在可抓取性能数据:

    Stats::Broadcast()
    {
        SCOPE_CYCLE_COUNTER(STAT_StatsBroadcast);
        // ...
        // 一串代码
        // ...
    }

Copy full snippet

Stats::Broadcast() { SCOPE_CYCLE_COUNTER(STAT_StatsBroadcast); // ... // 一串代码 // ... }

有时候不想每次调用函数时都抓取统计数据,可以使用条件循环计数器——这不是很常见,但可能很有用:

    Stats::Broadcast(bool bSomeCondition)
    {

        CONDITIONAL_SCOPE_CYCLE_COUNTER(STAT_StatsBroadcast,bSomeCondition);
        // ...
        // 一串代码
        // ...
    }

Copy full snippet

Stats::Broadcast(bool bSomeCondition) {

    CONDITIONAL_SCOPE_CYCLE_COUNTER(STAT_StatsBroadcast,bSomeCondition);
    // ...
    // 一串代码
    // ...
}

如要从一个函数抓取性能数据,可使用下列构造:

    Stats::Broadcast(bool bSomeCondition)
    {
        DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Broadcast"), STAT_StatsBroadcast, STATGROUP_StatSystem);
        // ...
        // 一串代码
        // ...
    }

Copy full snippet

Stats::Broadcast(bool bSomeCondition) { DECLARE_SCOPE_CYCLE_COUNTER(TEXT("Broadcast"), STAT_StatsBroadcast, STATGROUP_StatSystem); // ... // 一串代码 // ... }

也可进行下列操作:

    Stats::Broadcast(bool bSomeCondition)
    {
        QUICK_SCOPE_CYCLE_COUNTER(TEXT("Stats::Broadcast"));
        // ...
        // 一串代码
        // ...
    }

Copy full snippet

Stats::Broadcast(bool bSomeCondition) { QUICK_SCOPE_CYCLE_COUNTER(TEXT("Stats::Broadcast")); // ... // 一串代码 // ... }

这主要用于临时统计数据。

以上提到的所有循环计数器均用于生成层级,因此可获取关于性能数据的详细信息。但也可选择设置平循环计数器:

    Stats::Broadcast(bool bSomeCondition)
    {
        const uint32 BroadcastBeginTime = FPlatformTime::Cycles();
        // ...
        // 一串代码
        // ...
        const uint32 BroadcastEndTime = FPlatformTime::Cycles();
        SET_CYCLE_COUNTER(STAT_StatsBroadcast, BroadcastEndTime-BroadcastBeginTime);
    }

Copy full snippet

Stats::Broadcast(bool bSomeCondition) { const uint32 BroadcastBeginTime = FPlatformTime::Cycles(); // ... // 一串代码 // ... const uint32 BroadcastEndTime = FPlatformTime::Cycles(); SET_CYCLE_COUNTER(STAT_StatsBroadcast, BroadcastEndTime-BroadcastBeginTime); }

使用GetStatId的性能数据

有几种在虚幻引擎中实现的任务使用不同的方法来获取性能数据。它们实现方法 GetStatId(),如没有 GetStatId(),代码将无法编译。

请参见以下示例:

    class FParallelAnimationCompletionTask
    {
        // ...
        // 一串代码
        // ...
        FORCEINLINE TStatId GetStatId() const
        {
            RETURN_QUICK_DECLARE_CYCLE_STAT(FParallelAnimationCompletionTask, STATGROUP_TaskGraphTasks);
        }
        // ...
        // 一串代码
        // ...
    };

Copy full snippet

class FParallelAnimationCompletionTask { // ... // 一串代码 // ... FORCEINLINE TStatId GetStatId() const { RETURN_QUICK_DECLARE_CYCLE_STAT(FParallelAnimationCompletionTask, STATGROUP_TaskGraphTasks); } // ... // 一串代码 // ... };

记录性能数据

如只想记录性能数据,我们提供有下列功能:

方法

下列方法会捕获时间(以秒为单位传递),并将增量时间添加到传入的变量:

SCOPE_SECONDS_COUNTER(double& Seconds)

代码范例

    Stats::Broadcast()
    {
        double ThisTime = 0;
        {
           SCOPE_SECONDS_COUNTER(ThisTime);
            // ...
            // 一串代码
            // ...
        }
        UE_LOG(LogTemp, Log, TEXT("Stats::Broadcast %.2f"), ThisTime);
    }

Copy full snippet

Stats::Broadcast() { double ThisTime = 0; { SCOPE_SECONDS_COUNTER(ThisTime); // ... // 一串代码 // ... } UE_LOG(LogTemp, Log, TEXT("Stats::Broadcast %.2f"), ThisTime); }

工具类和方法

类 说明
FScopeLogTime 记录时间(以秒为单位传递)的工具类,将累加统计数据添加到传入的变量,并在析构函数中将性能数据打印到日志。
方法 说明
SCOPE_LOG_TIME(Name, CumulativePtr) 使用提供的名称打印性能数据并收集累加统计数据。
SCOPE_LOG_TIME_IN_SECONDS(Name, CumulativePtr) 与SCOPE_LOG_TIME功能相同,但以秒为单位打印。
SCOPE_LOG_TIME_FUNC() 它使用函数名称打印性能数据,而且不能嵌套。
SCOPE_LOG_TIME_FUNC_WITH_GLOBAL(CumulativePtr) 与SCOPE_LOG_TIME_FUNC功能相同,但它收集累加统计数据。

代码范例

    double GMyBroadcastTime = 0.0;
    Stats::Broadcast()
    {
        SCOPE_LOG_TIME("Stats::Broadcast", &GMyBroadcastTime);
        SCOPE_LOG_TIME_IN_SECONDS("Stats::Broadcast (sec)", &GMyBroadcastTime);
        // ...
        // 一串代码
        // ...
    }

    Stats::Condense()
    {
        SCOPE_LOG_TIME_FUNC(); // 命名应为"Stats::Condense()",在不同编辑器中可能有所不同
        SCOPE_LOG_TIME_FUNC_WITH_GLOBAL(&GMyBroadcastTime);
        // ...
        // 一串代码
        // ...
    }

Copy full snippet

double GMyBroadcastTime = 0.0; Stats::Broadcast() { SCOPE_LOG_TIME("Stats::Broadcast", &GMyBroadcastTime); SCOPE_LOG_TIME_IN_SECONDS("Stats::Broadcast (sec)", &GMyBroadcastTime); // ... // 一串代码 // ... }

Stats::Condense()
{
    SCOPE_LOG_TIME_FUNC(); // 命名应为"Stats::Condense()",在不同编辑器中可能有所不同
    SCOPE_LOG_TIME_FUNC_WITH_GLOBAL(&GMyBroadcastTime);
    // ...
    // 一串代码
    // ...
}

使用浮点或DWORD计数器的泛型数据

需要做的首件事通常是添加计数器,如下所示:

    DECLARE_FLOAT_COUNTER_STAT_EXTERN(STAT_FloatCounter,StatId,STATGROUP_TestGroup, CORE_API);
    DECLARE_DWORD_COUNTER_STAT_EXTERN(STAT_DwordCounter,StatId,STATGROUP_TestGroup, CORE_API);
    DECLARE_FLOAT_ACCUMULATOR_STAT_EXTERN(STAT_FloatAccumulator,StatId,STATGROUP_TestGroup, CORE_API);
    DECLARE_DWORD_ACCUMULATOR_STAT_EXTERN(STAT_DwordAccumulator,StatId,STATGROUP_TestGroup, CORE_API);

Copy full snippet

DECLARE_FLOAT_COUNTER_STAT_EXTERN(STAT_FloatCounter,StatId,STATGROUP_TestGroup, CORE_API); DECLARE_DWORD_COUNTER_STAT_EXTERN(STAT_DwordCounter,StatId,STATGROUP_TestGroup, CORE_API); DECLARE_FLOAT_ACCUMULATOR_STAT_EXTERN(STAT_FloatAccumulator,StatId,STATGROUP_TestGroup, CORE_API); DECLARE_DWORD_ACCUMULATOR_STAT_EXTERN(STAT_DwordAccumulator,StatId,STATGROUP_TestGroup, CORE_API);

之后,更新和管理计数器可使用下列方法:

用于更新计数器的方法

方法 说明
INC_DWORD_STAT(StatId) 使dword统计数据加1。
DEC_DWORD_STAT(StatId) 使dword统计数据减1。
INC_DWORD_STAT_BY(StatId, Amount) 使dword统计数据增加指定值。
DEC_DWORD_STAT_BY(StatId, Amount) 使dword统计数据减少指定值。
SET_DWORD_STAT(StatId, Value) 将dword统计数据设为指定值。
INC_FLOAT_STAT_BY(StatId, Amount) 使浮点统计数据增加指定值。
DEC_FLOAT_STAT_BY(StatId, Amount) 使浮点统计数据减少指定值。
SET_FLOAT_STAT(StatId, Value) 将浮点统计数据设为指定值。

用于管理计数器的助手方法

方法 说明
GET_STATID(StatId) 返回统计数据的 TStatId 的实例。 此为高级方法。
GET_STATDESCRIPTION(StatId) 返回统计数据的描述。