JincResize 代码重构(1)
前一篇 Blog 记录了将原作者代码(初步)从 C 语言迁移到 C++ 上的过程,以及添加 8bit 输入支持的方法。下一步我想模仿 HomeOfVapourSynthEvolution 的代码风格,对上述代码进行重构。同时,也期待能发现,我先前修改的代码运行缓慢的原因。
Blog 写得详细一些,这也是我学习 C++ 基础知识的过程。
未完成的 C 语言到 C++ 迁移
其实 C 语言和 C++ 的区别还有很多。
C++ 的类型转换
编写 VapourSynth 插件,在四个VS__CC
函数的开头,都需要进行类型转换,原始代码使用 C 语言风格的类型转换,类似下面这样。
1 |
|
如果让代码“更 C++”一些,则使用类型转换运算符(Type Conversion Operator)。
1 |
|
对于 filterInit ()
、filterGetFrame()
、filterFree()
三个函数,使用static_cast
进行转换。static_cast
用于相关类型的指针之间的转换。
对于filterCreate()
函数,使用reinterpret_cast
进行类型转换。无论类型是否相关,reinterpret_cast
均可进行(强制)类型转换,是最接近 C 语言风格的类型转换运算符。
However,上述改动对运行速度无可见影响(想一下也知道,使用类型转换运算符或许可以让代码更安全,但并没有涉及速度的问题)。
从指针到智能指针
书中的描述,给我的印象就是两句话:一是智能指针不是指针,是类,包含重载运算符的类(好像很多地方都这么说,用 A 修饰的 B 不是 B,JavaScript 不是 Java);二是智能指针要配合解除引用运算符*
和成员选择运算符->
使用,实现类似指针的效果(不能用.
)。
顺带一提,使用智能指针需要#include <memory>
。
智能指针的优势,就是能及时、更安全地管理内存。
在原始代码的fiterCreate()
函数部分,首先声明了FilterData
类型的变量d
和指针data
(虽然我从头看到尾,也没明白指针data
干了什么),在函数的最后,使用malloc()
函数为指针data
分配了空间,并赋值指向d
。
仿照HomeOfVapourSynthEvolution的代码风格,使用智能指针代替上述普通指针,就是将开头的声明由
1 |
|
改为
1 |
|
以及将使用.
的地方改为->
(为什么原代码要用.
呢)。
此外,还有一个事情,在函数体的最后,vsapi->ceateFiler()
那个部分,使用release()
函数将d
释放掉…我还不知道这是否是配合智能指针的写法,还是仅仅是一个普通的内存释放。
从智能指针到多态
《21 天学通 C++》中提到,智能指针在处理多态对象时具有优势。于是就翻到了多态的那一章,看到了虚函数相关的内容,便想起了在读 avs 版 JincResize 代码时,看到虚函数便一头雾水。
从宏定义到 std
原始代码使用了两个自定义的宏函数,MAX
和MIN
。先前已经将一部分MAX
和MIN
用std::max
和std::min
替代,但另一些无法替代。今天才发现,std::max
和std::min
的输入变量类型需要一致,自定义的宏函数则没有做规定(这应该也反映了宏的不安全)。那些不能替代的表达式,均是浮点型和整型混搭,改成一致后就可以用std::max
和std::min
了。
比较了一下宏、std、使用了模板的内联函数,三者的效率,发现还是使用std更快一点。(测试方法,测试了一段1200帧左右的短视频,比较时间)
内存管理相关
其实上面的智能指针就是关于内存管理的。这里单独拿出来,主要是想说一下new
、delete
、malloc()
、free()
。这也是我理解最困难的地方。
一方面,从一开始迁移代码就不明白的data = malloc(sizeof(d));
。另一方面,读HomeOfVapourSynthEvolution的代码,像在filterFree()
中,只对d
进行了delete
,而在原始代码中,使用了free()
,不仅释放了d
,还释放了d->lut
,我不明白为什么还要单独释放lut
这么一个对象,明明整个d
都释放了。同时,我还不知道delete
和free()
两种操作的区别(虽然我知道free()
和malloc()
配合使用,但我好像找不到相应的malloc()
啊)。
在前辈的指点下,阅读了这篇教程( https://www.includehelp.com/cpp-tutorial/difference-between-delete-and-free.aspx ),至少是知道了 delete
比free()
更快,因为前者是操作符,后者是函数。
其他一些细节
不是很关键的知识,还是记下来,免得忘了。
对于cmath
找不到 π 的宏定义M_PI
,可以在预处理中加入_USE_MATH_DEFINES
。
增加异常处理
这部分语法比较简单,在filterCreate()
函数中增加try {...} catch {...}
代码块,并把之前的内容 copy 到try {...}
内就 OK 了。需要思考的是会遇到什么异常。
真正意义上的重构部分
引子
在前一节的内存管理中也提到,我一直不太明白原始代码很多关于对象的“单独操作”的意义。除了上面的free(d->lut)
外,类似new_vi.width = d->w
等单独操作,也不太明白。话说回来,这两个例子是有区别的:后者删了的话,编译通过,但在 vs 中测试程序就崩溃;前者删不删都完全正常。
循环内计算与循环外调用
除法很慢,而原始代码中又存在可以移除 for 循环的除法计算,所以移除 for 循环,希望可以提升速度。
原代码
1 |
|
新代码
1 |
|
在实际使用中,oh
和ow
至少对应 1080p or 720p。对于 1080p,上述双重 for 循环就要循环 1920*1080 ≈ 2*10^6 次,即 200+ 万次,除法进行了 400+ 万次。我以为把除法放到循环外,能提高效率,没想到用上面的视频片段测试,反而更慢了…
记得在一个地方读到过,调用循环外的变量也存在开销,因外定义、调用变量就要调用析构函数(但我又看到过对于简单变量不需要析构函数…),进而降低效率。所以存在“循环内计算vs循环外调用”的取舍。
emmm我不知道该怎么解释了,也可能是我的测试方法依赖于当时 CPU 的表现?
除了这个除法的改动,我还试着把 for 循环内的位移运算拿出来。但大家都知道,位移运输很快,所以拿到 for 循环外后,速度下降得更明显了…我好难…
知乎上看到两句话,合起来就是,“不必追求 5% 效率的提升”,“这样可能会降低其他方面的性能,如可扩展性”。
虽然目前的改动,对效率的影响可能连 5% 都不到,但我还是要改的吧,毕竟上面的双重 for 循环内还有一个双重 for 循环…
目前代码的运算量
核心运算量是前前后后加起来的五重 for 循环,第一重循环 3 次,这倒是没什么,后面两个双重for循环,对于放大到 1080p 的情况,大约要执行 4.3*10^12 次,即 4.3 万亿次…乘 3,就是 12.9 万亿次…除了 for 循环本身的判断,循环内还有 if 判断语句(似乎这个也会拖慢性能)。
虽然作为放大算法,这恐怕也没办法..但看上去仍觉得可怕…
从 avs 版代码得到的启发
去看了 avs 版 JincResize 代码,原始版的核心代码和我现在的代码看上去差不多。但最新的代码,在第二个双重 for 循环中,省去了除法运算,也就是省去了归一化过程。怎么做到这一点的,我还没看明白,但这应该是一个优化的方向。
功能增加
添加 32bit 支持(2019.12.18)
在完成从 C 到 C++ 的迁移后,只支持 8-16bit 整型输入,若要增加 32bit 浮点数的支持,首先要修改一下模板函数的调用,再用float
型调用一次。
1 |
|
(这样看使用模板的理由就更充分了,如果说uint8_t
和uint16_t
都是整型,(或许?)可以用直接用int
型来操作,那么这次引入float
,模板的作用就更明显了。)
当然,仅仅接口处改了还不行,去看一下实际处理部分,也就是运算过程中有无涉及位深的部分。
唯一涉及的就是这句话
1 |
|
这种比较两次大小的操作在图像处理中很常见,用最大值和最小值限制,得到一个中间值的值,以防结果“溢出”色彩取值范围。对于整型而言,限制无非就是色彩[0,2^depth-1]
,比如熟悉的8bit,色彩取值[0,255]
,16bit 是[0,65335]
。而对于 32bit 浮点型,取值范围则是[-1.0,1.0]
,这样更好比较了,先判断一下stInteger
(vs中判断是否为整型的操作),若否,则
1 |
|
至此,运算部分对 32bit 浮点型的支持也完成了。
其他
使用 gcc 编译
主要参考了这篇文章: https://www.qiufengblog.com/articles/gcc-dll.html 。
使用 gcc 编译 C 语言代码,编译为 dll。
1 |
|
配置头文件和库文件路径我还不会..只好先到这,还是用 VS 吧。
(2019.11.25)成功使用 gcc/g++ 进行编译,并且编译后的 dll 运行速度大幅提升,和原作者 dll 的速度相似。
关于测试
借助 x265 编码器进行的测试,由于增加了视频重编码的过程,所以其他因素影响可能较大。
测试时有点意外的就是,我上面的所以改动(“从宏定义到 std”部分和“循环内计算与循环外调用”部分),最后重编码得到的文件,hash 值都一样。所以这些改动对运行结果没有影响,倒也放心了(特别把除法移除 for 的部分,我以为新声明一个变量储存除法结果,可能导致精度下降,但其并没有)。
下一步的计划
总之,现在就是从 free 和 delete 入手,开始代码重构;同时看多态的部分,争取迁移 avs 版的 jinc 函数实现。