本文为大便一箩筐的原创内容,转载请注明出处,谢谢:http://www.cnblogs.com/dbylk/p/4984530.html


公司引擎目前是使用CPU计算骨骼动画(采用了D3DX提供的函数进行计算)在屏幕中存在大量角色时仍然对CPU造成了不小的压力。根据VTune的性能检测结果,300人同屏时,D3DXMatrixMultiply函数占用了5%的CPU时间(仅次于DrawCall的开销),因此我想能不能把骨骼动画的向量矩阵运算转移到GPU中进行计算(即把骨骼相关的运算写在着色器中),但通过打印公司模型的骨骼数量,发现有不少模型的骨骼数目超过了70,最多的有87根。因为公司的游戏是基于Dx9开发的,顶点着色器最多只支持256个常量寄存器,即使使用4x3矩阵也放不下这么多骨骼(除非让美术。。。)。

而且我也不能保证在公司的项目中使用GPU计算骨骼动画对性能的影响一定是正向的。因为刚来公司的时候,导师就让我写了一个播放模型动画的小demo作为训练,最开始我是用C++写骨骼动画,后来自己又用空余的时间写了一版用着色器计算骨骼动画的demo,结果性能对比发现C++计算骨骼动画的平均fps在500左右,而着色器计算骨骼动画的平均fps在4000左右,整整差了8倍!(不过这应该也跟我计算骨骼动画的C++代码效率写得不高有关,因为我当时用的是自己写的空间变换矩阵生成函数和矩阵向量乘法函数。不过根据一些论坛里的前辈提供的经验,即使使用SIMD技术对我写的函数进行优化,效率提升应该也在3倍以内,不至于造成如此大的差距。)为此我专门去问了一下导师,导师说他曾经也尝试过使用着色器计算骨骼动画,但是发现帧数反而更低了,所以一直没有对公司引擎的这一部分做修改,如果我有兴趣的话可以自己改一下,对比一下效率。然而这话说完没多久,导师就跳槽了,所以目前本人处于无人指导,自己胡乱摸索的阶段。。。小公司的悲哀T_T。。。

言归正转,因为导师不在公司了,所以我也没有办法知道他之前测试的时候着色器计算骨骼动画为什么会帧数更低的细节。虽然从理论和常识上来看,GPU应该比CPU更适合做这方面的运算,但考虑到造成游戏帧数并不单单只受限于CPU或GPU的运算性能,还会受到CPU/GPU内存同步、硬盘读写、网络状况等等各方面因素的制约,所以我也不敢贸然下定论。况且改写这方面的代码是一个大工程,不是一时半会就能改完的,如果写出来效率不如以前的话心血就白费了。。。为此我就想看看网上有没有前辈对“在CPU与GPU计算骨骼动画的性能”方面写过相关的分析与对比,搜到的结果一边倒——骨骼动画使用GPU计算性能更高。不过也有不少人提到了常量寄存器对骨骼数目的限制因素,想想公司项目模型的87根骨骼,我的心又凉了半截。不过很快,大便我搜到了下面这篇博客:

一种简单有效的3D模型的动画多线程方案

看完后,我觉得文章中提到的技术实用性很高,于是我便打算在公司的项目中尝试一下。考虑到既然是使用CPU计算骨骼动画,要想让性能达到极致,怎么能忘了之前提到的SIMD技术。然而大便我之前对SIMD只是有所耳闻,并没有亲自使用过,所以自然要再搜索一番 —3—)。。。

结果搜到了下面这个东西:

为什么使用SSE指令没有性能提升

上面这篇贴子的楼主在13楼回复了下面这段话:

TimothyField:
 
这个问题昨天晚上已经基本解决,因为我已经连续发了3个帖子,系统不让我继续发,所以没有及时更新。首先要感谢polytechnic的提醒,我又仔细检查了各个部分单独花的时间,因为没有合适的工具,我是通过简单注释掉部分代码看执行时间的变化来查找疑点的。前面提到注释掉SSE代码的时候我是把相关的代码也注释掉了,现在再降低注释的粒度。首先注意到其实性能瓶颈确实不在SSE代码部分,而是FastExp函数。这确实有点出乎意料,因为这个函数只是简单的一个查表:
inline float TFastExp::Exp(float x)
{int n = (int)100*x;return data[n];
}
由于知道x的范围,所以连参数检查都没有,这样的一个函数怎么会成为性能瓶颈呢?我刚开始是怀疑由于n的取值变化比较大,所以data[n]的访问导致大量的cache missing,所以专门写了一段类似的程序模拟测试,数组的索引用n*31%size模拟随机访问(random函数太慢了),结果并没有发现类似的现象。于是唯一的一个可能原因就是浮点数到整数的转换了。C编译器产生的浮点到整数的转换比较慢我是知道的,但到底多慢就没有概念了,好在验证起来比较简单,我把n设置为一个固定的整数,执行时间一下子就缩短了。知道原因之后就比较容易解决了,现在已经把这个函数改写为:
float TFastExp::Exp(float x)
{int n;float y = 100*x;_asm fld y_asm fistp nreturn data[n];
}用两条汇编指令,6个时钟周期搞定。(因为inline函数中不能使用嵌入式汇编,所以这个函数不再加上inline)这个地方修改之后,程序执行时间一下降低到106秒。平均单个循环只需要150个CPU TICK左右,比较原来需要570个CPU TICK,可以猜测一个浮点数到整数的转换在C++ Builder的缺省实现中需要约400个时钟周期!!!这个猜测比较吓人,但确实是现在得到的数据暗示的结论。再重新比较一下不使用SSE指令的C++版本算法,实测执行时间是248秒,也就是说使用SSE指令进一步循环展开后,执行时间降低到不使用SSE版本的约1/2.5。这跟原来期望差不多了。

“浮点数到整数的转换”,这不跟我之前优化的那个GetMatrixKey函数有关系吗?!

下面要介绍一下GetMatrixKey这个函数(我会关注到它完全是因为VTune,否则这么一个小函数根本想不到它会成为性能杀手,占用的CPU时间仅次于D3DXMatrixMultiply排在第三)。在我第一次看见它的时候,它是长这样的:

// Author:大便一箩筐)
D3DXMATRIX* XXXXX::GetMatrixKey(KeyMatrix* pArray, int nCount, int nFrame) {if (nCount == 0) {return NULL;}
    // 帧数一定是i, i+1, i+2…连续输出的int nStartFrame = static_cast<int>(pArray[0].Frame);if (nStartFrame >= nFrame) {return &pArray[0].Matrix;}if (nFrame >= GET_END_FROME_START(nCount, nStartFrame)) {return &pArray[nCount - 1].Matrix;}if (int(pArray[nFrame - nStartFrame].fFrame) != nFrame) {printf("\n帧数%d 起始帧%d 结束帧%d %s\n", nFrame, nStartFrame, int(pArray[nFrame-nStartFrame].fFrame), __FUNCTION__);}return &pArray[nFrame-nStartFrame].Matrix;
}// 函数中用到的GET_END_FROM_START宏定义如下
#define GET_END_FROM_START(nCount, nStart) ((nCount)+(nStart)-1)// 函数参数中用到的KeyMatrix参数定义如下
class KeyMatrix {
public:float fFrame;D3DXMATRIX Matrix;
}

首先我要吐槽一下KeyMatrix这个类:

  • 我不知道为什么表示变换的矩阵要和它对应的帧数一起存在这样一个类里(根据搜索结果fFrame除了这个函数根本没有其他地方用到)
  • 而且为什么要把帧数fFrame定义成浮点类型(根据这个函数原来有的注释:“帧数一定是i, i+1, i+2…连续输出的”,可以知道fFrame是整数,所以这里用到的时候要把它转成int)

因为KeyMatrix类被用在了动画类里,它所涉及的数据都被存在了游戏模型的动画文件里,所以贸然修改它不是一个明智的决定。

“GetMatrixKey这个函数的作用是根据输入的帧数nFrame返回pArray数组中对应的KeyMatrix中的矩阵。”

上面这个结论是我盯着这个函数看了几分钟以后才得出的,因为这个函数中使用了一个宏定义“GET_END_FROM_START”,让我初看时认为这个函数一定非常复杂。结果把宏定义套进函数再仔细一看,才发现这个函数的主要作用就是做数组范围检查,判断nFrame有木有越界!一个检查数组越界的函数写得如此复杂(各种重复计算,在频繁调用的函数里执行不必要的打印,使用没有必要的宏定义),简直不能忍。。。

随后,我把这个函数简单地修改了一下:

// Author : 大便一箩筐 
inline D3DXMATRIX* XXXXX::GetMatrixKey(KeyMatrix* pArray, int nCount, int nFrame) {if (!nCount) {return NULL;}int nStartFrame = static_cast<int>(pArray[0].fFrame);int nIndex = nFrame - nStartFrame;if (nIndex < 0) {nIndex = 0;}if (nIndex >= nCount) {nIndex = nCount - 1;}return &pArray[nIndex].Matrix;
}

修改以后,我又用VTune测了一下性能,发现此函数的CPU时间降到了修改前的40%,虽然优化效果比较明显,但依然占用了不少的CPU时间。“这么一个简单的函数也要占用这么多CPU时间,也许是调用的次数太多了吧”,当时我是这么想的。

现在看了CSDN这篇贴子,原来这个函数的性能消耗主要是在不起眼的基本数据类型的转换上,着实给我上了一课。

我马上打开VS2013,用之前自己写的性能测试工具测了一下float到int直接转换与CSDN贴子中楼主TimothyField提供的方法的开销,结果却让我感到非常意外——VS2013的Debug模式下编译出来的程序,在执行50,000,000次转换时,float到int直接转换消耗的时间比TimothyField提供的方法消耗时间少0.8s,也就是说直接转换的效率更高。这让我感到非常奇怪,但大便我马上注意到了TimothyField在贴子中提到他使用到编译器是C++ Builder,“也许是VS的编译器在转换中做了优化,使它比TimothyField提供的汇编更高效?”。为了确认这一点,我打开了VS调试模式中的反汇编窗口,想看看这两种转换的汇编代码有什么不同,结果发现了下面这个指令:

cvttss2si   eax,xmm0

马上打开网页搜索了一番,发现原来这个指令也是SSE指令集中的指令,它的作用是提供更高效的float到int的截断型转换。想必是C++ Builder并没有在默认转换中使用这个指令,才使得他的默认转换比fld和fistp指令更低效。

然而公司项目使用的还是VS2008编译器,会不会也没有默认使用cvttss2si指令呢?实践出真知,我马上按下了F5,打开反编译窗口查看了相应的汇编指令,发现VS2008果然没有使用cvttss2si指令,而是调用了一个float转int的函数(当时忘记给相应的汇编指令截图了,名字忘记了)。

我迫不及待地想要把公司项目中的float到int型的转换全部替换为cvttss2si指令了,不过还是再单独测试一下这个指令的效率比较好,于是我参考了VS2013直接转换的反汇编,又写了一个函数做测试:

// Author : 大便一箩筐inline void SseAsmCast() {for (int i = 0; i < nCalculation; ++i) {float fTemp = fDenominator * fNumber;int iTemp;_asm cvttss2si eax, fTemp_asm mov       iTemp,eaxfNumber = fTable[iTemp];}
}

然而测试结果却再一次让我大跌眼镜,即使使用了cvttss2si指令,消耗的时间也和使用fld + fistp指令一样,远低于VS2013默认转换的效率。为此,我考虑到可能VS2013在默认转换的过程中优化掉了临时变量iTemp与fTemp,直接使用32位寄存器(eax/ebx/ecx/edx)存储中间结果,所以才会有更高的效率,于是我又增加了几条汇编指令,避免了了iTemp与fTemp的定义:

// Author : 大便一箩筐 
 
inline void SseAsmCast() {for (unsigned int i = 0; i < nCalculation; ++i) {_asm {movss        xmm0, fNumbermulss        xmm0, fDenominatorcvttss2si    eax, xmm0mov          ebx,fTablemovss        xmm0,dword ptr [ebx+eax*4]movss        fNumber,xmm0}}
}

这一次,在Debug模式下,汇编指令的效率超越了直接转换的效率,但当我使用Release模式测试时,发现VS2013的直接转换效率再次超越了上面的汇编指令。

为此,我又查看了一下Release模式下的反汇编代码,发现VS在Release模式下还做了一个优化,那就是省略了循环体中的“movss xmm0,fNumber”这条指令,直接使用上一次循环中的xmm0寄存器参与乘法运算,为了验证,我又将汇编指令的转换函数改写如下:

// Author : 大便一箩筐

inline void SseAsmCast() {_asm movss        xmm0, fNumberfor (unsigned int i = 0; i < nCalculation; ++i) {_asm {mulss        xmm0, fDenominatorcvttss2si    eax, xmm0mov          ebx,fTablemovss        xmm0,dword ptr [ebx+eax*4]movss        fNumber,xmm0}}
}

这一次的测试结果证实了我的想法,上面的汇编指令与VS2013编译出来的直接转换效率相当,甚至还要稍微高效一点(Release模式下50,000,000次转换节省0.03s,整个函数约有10%的效率提升)。

最后得出的结论是:如果发现你所使用的编译器没有使用SSE指令执行float到int型的转换,可以手动使用内联汇编对程序进行优化

整个验证程序的源码如下:

// Author : 大便一箩筐#pragma comment(lib, "TestUtils.lib")#include "../TestUtils/DB_Log.h"
#include "../TestUtils/DB_Timer.h"#include <iostream>using namespace std;
using namespace DaBianYLK;#define FLOAT_TO_INT(f, i) _asm fld f _asm fistp ifloat* fTable = new float[1024];
const float fDenominator = 3.3f;
float fNumber = 1.0f;
const unsigned int nCalculation = 50000000;inline void SetupFloatTable() {for (unsigned i = 0; i < 1023; ++i) {fTable[i] = (i + 1 + 0.33f) / fDenominator;}fTable[1023] = 1.0f / fDenominator;
}inline void DirectCast() {for (unsigned int i = 0; i < nCalculation; ++i) {int iTemp = fDenominator * fNumber;fNumber = fTable[iTemp];}
}inline void SseAsmCast() {_asm movss        xmm0, fNumberfor (unsigned int i = 0; i < nCalculation; ++i) {_asm {mulss        xmm0, fDenominatorcvttss2si    eax, xmm0mov          ebx,fTablemovss        xmm0,dword ptr [ebx+eax*4]movss        fNumber,xmm0}}
}inline void NormalAsmCast() {for (unsigned int i = 0; i < nCalculation; ++i) {float fTemp = fDenominator * fNumber;int iTemp;_asm fld   fTemp_asm fistp iTempfNumber = fTable[iTemp];}
}inline void StaticCast() {for (unsigned int i = 0; i < nCalculation; ++i) {int iTemp = static_cast<int>(fDenominator * fNumber);fNumber = fTable[iTemp];}
}int main(void) {SetupFloatTable();// 直接转换fNumber = 1.0f;BENCHMARK(DirectCast, DirectCast());Log("FNumber : %f", fNumber);// TrickfNumber = 1.0f;BENCHMARK(SseAsmCast, SseAsmCast());Log("FNumber : %f", fNumber);// TrickfNumber = 1.0f;BENCHMARK(NormalAsmCast, NormalAsmCast());Log("FNumber : %f", fNumber);// 静态转换fNumber = 1.0f;BENCHMARK(StaticCast, StaticCast());Log("FNumber : %f", fNumber);            // 至少要输出一次fNumber,否则编译器的优化会删除执行运算的代码
system("pause");return 0;
}

其中BENCHMARK宏是我编写的性能测试工具,它的源码开放在了我个人的GitHub:

https://github.com/DaBianYLK/TestProjects

转载于:https://www.cnblogs.com/dbylk/p/4984530.html

尝试优化骨骼动画计算的意外收获——使用嵌入式汇编对float转int进行优化相关推荐

  1. Unity3D插件 AnyPortrait 2D骨骼动画制作

    一.前言 AnyPortrait是一个创建2D角色动画制作的Unity拓展编辑器插件. AnyPortrait提供了很多功能,让你可以在Unity里面就完成动画的制作. 使用AnyPortrait插件 ...

  2. Skeletal Animation(骨骼动画)

    Skeletal Animation(骨骼动画) 有关骨骼动画的东西都放在这里好了. http://en.wikipedia.org/wiki/Skeletal_animation Skeletal ...

  3. DirectX12(D3D12)基础教程(十七)——让小姐姐翩翩起舞(3D骨骼动画渲染【5】)

    目录 10.动画关键帧解算 10.1.时间轴 10.2.遍历动作CalcAnimation 10.2.递归遍历骨骼树ReadNodeHeirarchy 10.3.关键帧数据解算和插值 10.4.生成关 ...

  4. 在C++中自己实现动画系统(一):骨骼动画与编辑器

    转:https://www.gameres.com/478091.html 每套游戏引擎都会包含动画系统:一些游戏引擎会采用最简单的直播动画的形式(如Unity的Animation方案)仅支持简单动画 ...

  5. Unity骨骼动画优化

    一:骨骼动画的原理 用Unity做游戏经常会用到3D角色,也就会用到骨骼动画.骨骼动画对性能的影响其实非常大的,在说这个问题之前,先来说说骨骼动画的原理. 我之前做过多种骨骼动画,包括写过2D的骨骼动 ...

  6. EntityFramework Core不得不注意的性能优化意外收获,你会用错?

    前言 这两天在着实研究EF Core项目当中对于一些查询也没实际去检测,于是想着利用放假时间去实际测试下,结果本文就出来了,too young,too simple,后续博主会从底层翻译表达式树弄起, ...

  7. 3D动画概述暨骨骼动画实现

    引言 本文论述了3D领域内的常见动画类型的运作机制.不同于其他文章简单的罗列和介绍每种类型的3D动画,本文尝试以一种优化演进的思路对动画运作机理进行递进式推演,在这个过程中自然而然的推导出常见的几种3 ...

  8. webGL、webGPU、封装、渲染引擎 three.js、游戏引擎,定位是游戏开发,在前面的渲染引擎基础上,还提供了骨骼动画、物理引擎、AI、GUI 等功能,以及可视化编辑器来设计关卡,支撑大型游戏

    https://zhuanlan.zhihu.com/p/162878354 如何选择 WebGL 框架和引擎? ​ 知道得越多,不知道的就更多了 数据可视化Sugar-百度智能云 ​cloud.ba ...

  9. 关于骨骼动画及微软示例Skinned Mesh的解析

    这是我自个写的,第一次发. 没想到这个贴子编辑器极差. 原文是有字体字色的.现在只能清一色了.    版主,发贴的编辑器太难用! 你有必要向上反映一下. 下面的字体是我敲html标记加上的,大家凑和看 ...

最新文章

  1. 自监督学习新思路!基于蒸馏损失的自监督学习算法 | CVPR 2021
  2. linux 内存管理 page fault带来的性能问题
  3. 【虚拟化】Linux中安装配置Docker
  4. windows远程连接ubuntu 黑屏_Windows跟Windows远程连接传输文件
  5. 修复 XE7 , XE8 Frame 内 PopupMenu 快捷键失效问题
  6. 2019安装软件安装管家_【软件安装管家】Br 2019软件安装包+安装教程
  7. NGUI_2.6.3_系列教程六(序列帧动画)
  8. 第四章 ---- 事务RedisTemplate
  9. Trick(一)——判断一个数的位数
  10. python统计数组元素个数_统计二维数组里元素的个数
  11. [六省联考2017]相逢是问候(线段树+拓展欧拉定理)
  12. find 命令详解 基于文件大小,名字和权限等的查找策略以及-path和-prune的详细解释
  13. 操作系统–银行家算法c语言代码
  14. 图书馆占座系统(SSM,JQUERY-EASYUI,MYSQL)
  15. win7 mysql 管理员权限_win7 管理员权限
  16. C#实战之CAD二次开发002:绘制直线和绘制圆
  17. 计算机屏幕闪烁黑屏,台式机电脑。显示屏指示灯一直闪烁,屏幕黑屏。。...-显示器电源灯闪黑屏...
  18. ubuntu(Linux) 挂接小鹤音形 (基于IBus框架)
  19. [计算机网络] 拥塞控制
  20. 移动终端及常见的操作系统

热门文章

  1. MUI+H5手机上传照片 支持多图片上传和拍照上传
  2. python建立数据库并搜索_如何建立一个简单的数据库,可供人在网络上进行搜索?...
  3. c语言 二进制输出_程序员入门C语言,需要掌握的4个基础知识
  4. 解决:The application could not be installed: INSTALL_FAILED_SHARED_USER_INCOMPATIBLE
  5. 37镇魔曲网页版服务器状态,37镇魔曲网页版各职业攻略分析
  6. 人脸检测算法_目前最强!开源人脸检测算法:RetinaFace
  7. Vue+ElementUI纯前端技术实现对表格数据的增删改查
  8. TypeScript学习笔记3:运算符
  9. 2021-03-09 Matlab RBF神经网络及其实例
  10. pandas中的DataFrame数据结构