游戏引擎学习第114天

ops/2025/2/21 13:16:36/

打开内容并回顾

目前正在讨论一个非常重要的话题——优化。当代码运行太慢,无法达到所需性能时,我们该怎么办。昨天,我们通过在代码中添加性能计数器,验证了一些性能分析的数据,这些计数器帮助我们了解每个操作需要的时间。今天,我们希望进一步探讨如何基于收集到的性能数据,采取相应的优化措施。

在之前的工作中,回顾之前的内容,我们查看了 DrawRectangleSlowly 函数,测试时将一些周期计数器输出到调试端口,展示了每一帧的性能数据。例如,我们发现每填充一个像素大约需要debug 2016个周期。 release 83 个周期。

现在的目标是考虑在合理的时间内将每个像素的处理周期缩短到什么程度。为了做到这一点,需要重新审视 DrawRectangleSlowly 函数。最初编写这段代码时,我们只是想要确保它能够正常工作,主要是学习如何绘制纹理映射三角形,并没有特别关注代码的优化或者性能。因此,接下来需要更仔细地思考如何组织和优化这段代码。

DrawRectangleSlowly: 提高效率

接下来需要做的是,看看能否对现有的代码进行一些转换,尽量将其简化,减少不必要的计算,使其更加简洁清晰。我们需要分析这段算法,看看实际上需要执行多少个操作。这将帮助我们估算出代码的性能峰值,并为我们设定一个可以努力的目标。

具体来说,在 DrawRectangleSlowly 这部分代码中,目标是通过优化来减少操作量,同时了解优化后的算法能够带来什么样的性能提升。通过这一过程,我们可以确定实现的目标,进而更有效地进行优化工作。

创建 DrawRectangleHopefullyQuickly

接下来的目标是将 DrawRectangleSlowly 函数提取出来,创建一个新的版本,称为 DrawRectangleQuickly,或者更具创意地命名为 DrawRectangleHopefullyQuickly。通过这种方式,我们可以对现有的代码进行优化,使其更加高效。

为了进行优化,首先会去掉一些不必要的部分,比如暂时移除法线映射(normal mapping)部分,因为当前重点不在这个方面。然后,保持其他功能不变,专注于优化 DrawRectangleQuickly 函数,简化其操作流程,去除冗余的计算。接下来,还需要为这个新函数添加性能计数器,以便追踪和评估性能改进。目标是使这个新版本的代码在操作上更加简洁和高效。
在这里插入图片描述

在这里插入图片描述

DrawRectangleHopefullyQuickly: 跳过前导代码

现在,我们知道其实前面的准备工作部分并不是我们当前需要过多关注的部分。原因是,如果我们尝试优化这一部分,但它并没有在每个像素的循环中被调用,那么优化它对性能几乎没有影响。根据性能计数器的数据,我们知道大部分时间都花费在像素循环中,而不是在矩形的处理上。这也很有道理,因为像素数量远远多于矩形数量,因此大部分时间都花费在处理像素上,而与每个矩形相关的操作影响较小。

接下来,我们打算暂时保留这些时间块作为测试像素。我们不会立即调用这些时间块,但可以在切换这两个例程时进行一些实验。关键是,虽然不在乎最终调用哪个例程,但可以在这两个例程之间切换,查看效果如何。

移除所有不必要的代码

首先,我们会去掉当前不需要的代码。比如,将之前的法线映射代码移除。这是因为在当前优化过程中,法线映射并没有参与计算,因此不需要保留这些部分。确保在删除代码时不会误删掉其他必要的部分,经过检查后确认删除的内容正确无误。

接下来,将原本的 DrawRectangleSlowly 替换为 DrawRectangleHopefullyQuickly,而且这个新的版本并不会传递法线映射数据,因为在之前的位图绘制过程中也并没有涉及到法线映射。

完成这些调整后,检查一下程序是否仍然能够正常运行,确认一切正常。最终,检查到的时间也保持不变,这与预期一致,表明目前的调整并没有引起性能变化。
在这里插入图片描述

查看发生了什么

在优化 drawRectangleHopefullyQuickly 这一过程时,需要仔细审视循环中的每个操作,特别是那些涉及向量运算的部分。首先,检查并去除一些不必要的向量操作,以便更加清晰地理解函数内部的实际处理过程。

在循环内,我们可以看到一些看似直观的操作,比如按像素推进,这是完全合理的。但是,某些部分的操作,比如 x 轴减去 y 轴和负值的计算,实际上可以简化成一些基础的标量运算,这部分内容在向量形式下可能显得不太有效率。如果这些值在循环中是常量,重复的减法运算就显得多余了,实际上可以通过在循环外部进行优化,避免在每次迭代中进行不必要的计算。
在这里插入图片描述

使边缘测试代码更明确

在这段代码中,讨论的主要是通过内积运算来简化计算过程。首先,计算了向量 v负x轴 方向的垂直向量的内积。之后,将这些计算进行了映射,发现交换了两个元素的位置。通过这种方式,计算得到了所需的结果。实际上,计算的内积可以简单地写成两个元素的交换形式,这样就能直接得到正确的结果。

通过对这些步骤的分析,发现其实这只是一个非常直观的计算过程,之前的复杂度可以通过这种简化的方式降低。此外,进行类似的处理时,观察到还有一些附加的变化,这些变化也与原始的计算过程相关。

总结而言,整个过程通过优化内积计算,去除了多余的计算步骤,从而简化了原有的计算过程,提高了效率。同时,注意到有一些附加的细节,可能会影响到最终的计算结果。
在这里插入图片描述

Blackboard: 查看这些内积的情况

当前计算方法的优化思考。首先,提到在某些情况下,可能存在额外的优化空间,尤其是在垂直向量计算的部分。通过对垂直向量的反复计算,可以观察到,如果知道某个轴是与x轴垂直的,那么再对其进行垂直操作后,结果将是指向负方向的x轴。这种观察为进一步优化提供了线索。

接着,分析了当前代码中关于内积的计算,特别是与像素向量 d 和x、y轴的内积计算。提出可以通过预先归一化x轴,在开始计算之前就对其进行标准化,这样可以简化内积的计算,仅需关注如何沿着x轴的方向进行偏移,从而避免了对所有参数的多次检查。

进一步思考了代码的效率,认为通过只计算一次内积并进行边界测试,可以大大简化过程。与此类似,在纹理映射中,计算u和v值的过程可以告知是否位于位图的内部,这也表明可以通过这种方式进一步减少计算复杂度。

综上所述,主要的目标是通过减少重复的计算步骤来提高效率,同时探索了如何通过优化内积计算来简化并加速现有代码,进而提升整体性能。
在这里插入图片描述

在这里插入图片描述

DrawRectangleHopefullyQuickly: 测试 U 和 V

考虑到简化处理,可以忽略边缘的测试,而仅关注两个角点的坐标。通过计算u和v坐标并检查这些坐标是否位于方形区域内部,可以避免复杂的边缘处理工作。这种方法被认为相对简单,且能减少不必要的计算。还需要进一步思考如何确定点的“内部”或“外部”,以及选择合适的测试方式。另外,可以考虑删除某些变量,首先尝试简化代码,看看是否仍然有效。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏

经过测试,发现其实并不需要保留某些功能,结果反而使得游戏运行得更快,效果出乎意料。具体来说,通过简化处理,游戏的性能有所提升,运行速度得到了优化。这个改动使得游戏的速度略微加快,虽然最初没有预料到会有如此明显的改进。
在这里插入图片描述

出现了两个英雄的看一下什么原因导致的
在这里插入图片描述

在这里插入图片描述

使 U 和 V 的计算更高效

考虑到当前的方法,其实做了过多的计算。可以通过将x轴乘以其长度的平方,来简化过程,这样就不需要每次都进行乘法运算了。通过这种方法,计算可以更加高效。此外,可以通过替换计算中的x轴和y轴,进一步简化流程,最终达到更为简洁的实现。这种改进使得计算过程更直接,也避免了重复的复杂操作。

运行游戏,确保一切正常

这个方法看起来非常基础且直接,操作简单明了。测试过程中,一切运行正常,没有出现问题。为了确保没有遗漏任何细节,还验证了是否正确调用了裁剪矩形函数。此外,还在调试模式下检查了相关内容,确保每一行代码都得到了适当的执行。

继续修剪

在优化过程中,逐步去除了一些不必要的内容,简化了流程。首先,屏幕空间的电影效果在不使用法线映射时不再需要,因此被移除。类似地,一些冗余的代码,如 zdf 和其他不必要的部分,也被去除。由于现在已经在循环中进行检查,所以不再需要担心限制问题。接着,处理了纹理的边界乘法,减去2的部分仍需进一步考虑。为了确保值在合理范围内,还加入了断言检查,在发布模式下这些检查会被编译掉。

纹理采样部分也进行了优化,处理了像素打包,进入线性空间后进行了线性插值,最终重新打包。整体来看,代码已经开始简化,冗余的部分被剔除。关于颜色值的限制,虽然在大多数情况下不需要进行限制,但为了处理可能超出范围的颜色值,保留了限制操作,因为在当前的处理模式下,限制操作非常轻量。如果未来需要进行颜色放大操作,如将颜色乘以2,也可以轻松实现。这些优化的最终目的是减少不必要的计算,同时保持灵活性。
在这里插入图片描述

扁平化例程

为了更好地理解和查看代码的运行,先将剩余的部分注入到当前的例程中,进行整体查看。这段代码的目的是将输入数据转换到目标空间,通过直接对数据进行平方运算来实现。为了确保正确,首先需要将活动5移到合适的位置。然后,进行平方操作,即通过将值与自身相乘来实现。

接下来,进行逆平方操作,将数据保持在该空间内。然后,进行亮度空间的转换,最后将其转换为线性RGB空间。整个过程通过“扁平化”代码,使得所有步骤都集中在一个地方,方便查看和分析,而不需要分散到多个例程中。

尽管有些步骤看起来可以直接在原地计算,但为了代码的一致性,还是按原方式执行。所有的操作完成后,再次检查代码运行是否正常,确保一切看起来都很正常,结果符合预期。
在这里插入图片描述

将 v4 Blended 扁平化为标量形式

在分析过程中,发现这里有一些广义操作,意味着它们是在一次性处理一个向量,而不是单个标量值。这些操作涉及对向量进行处理,例如对 Dest 进行操作时,首先将其转换到目标空间后,紧接着执行 Blended = (1.0f - Texel) * Dest + Texel 这些运算。这些步骤在 Dest 数据被处理后立即执行。

所有相关的操作基本上都是相同的,涉及到将数据转化为某种形式(例如标量形式)后,进行类似的处理。这些操作的目的是统一处理多个值,而不仅仅是单个数据项,从而提高计算效率。
在这里插入图片描述

仔细查看例程并预计算 InvTexelA

通过检查代码,发现整个过程相对直接明了。首先,通过平方操作将数值转换到线性亮度空间,这一点是合理的。接着,计算 texelA 的值并使用它,这一过程没有复杂的逻辑,只是将 texel 值与其他值进行乘法运算。

在将这些操作组合在一起时,并没有找到任何可以进一步简化的部分。所有的步骤都只是必要的乘法操作,无法通过合并来优化。这些操作看似简单且直观,虽然有一些地方看起来有些不太一致,例如某些地方使用了 255,而其他地方没有,但这并未影响整体的逻辑。

接下来,可能需要关注双线性插值部分,因为目前的代码已经涵盖了所需的大部分操作,并没有太多需要进一步修改的地方。
在这里插入图片描述

在这里插入图片描述

将 v4 Dest 和 Texel 扁平化为标量形式

将代码中的操作显式化后,目的是清晰地看到每一步的具体实现,而不是隐藏在多个工具函数后面。通过展开,将 dest 值以标量形式处理,进行解包操作,然后对解包后的值进行相应的计算。操作包括将 texel 值进行处理,并且所有步骤都被明确地写出来,去除了隐式操作。

唯一没有显式处理的部分是一个特定的操作,其余部分已经完全展开为标量操作。最终,所有需要执行的步骤都可以清晰地看到,确保每一部分的功能都明确,没有遗漏。整体来看,代码依然运行正常,未出现任何问题。
在这里插入图片描述

扁平化 BilinearSample 和 SRGBBilinearBlend

在进行标量操作时,观察到使用 sRGB 双线性插值时会涉及多个解包操作,这使得代码变得相当复杂。尽管大部分操作比较简单,最终组合成的操作仍然是庞大的,尤其是进行纹理采样时需要执行多个解包和插值步骤,这让整个过程变得繁琐且低效。

虽然整个过程涉及的操作并不多,但因为需要进行解包、插值和双线性操作,导致这些操作在执行时变得非常麻烦且复杂。尽管如此,这些操作仍然是必须的,因此即使过程显得繁琐,还是需要实现这些功能。

实际操作时,使用了合适的纹理坐标变量,并通过双线性插值和纹理采样来获取最终的纹理值。每个采样点(如A、B、C、D)都会参与插值计算,最终得到一个纹理的值。
在这里插入图片描述

评估当前情况

目前例程已经扁平化,但仍然需要关注一些部分。这些部分相对直接,没什么复杂的地方。唯一的优化点是,可能可以预先知道这些偏移量,这样就不需要额外进行加法运算了。编译器可能会将这些操作折叠为常量,因此目前这样处理是完全可以的。

在这个过程中,有些解包操作和Texelrgb(如果线性解包存在)需要进行处理。接下来还需要解包一些Lerp,这些部分比较简单,接下来的工作是处理这两个Lerp,并且计算出它们的结果。

vscode多光标编辑选中字符ctrl+d

解包并优化 Lerps

这个过程虽然看起来比较复杂,但其实可以通过分解来简化。首先,我们需要计算一个中间值,这有点像是进行标量乘法。对于浮点数的操作来说,首先需要进行标量运算,处理每个元素时会执行相同的操作。这实际上是通过查找操作来实现的,其中一个部分是 (1 - fy) * part1,另一个部分是 fy * part2。为了让这个过程更容易理解,可以将 fy1 - fy 分别称为 ify1-fy,这样就能让每个操作的表达式更直观。

接下来,通过分配律,可以将 fy 分别乘到两个项上,去掉括号。这样就能得到三个混合系数,分别是 L0L1L2L3,它们是最终需要的系数。经过这个操作,整个双线性插值就变得非常简单,最终的操作只需要进行四次乘法和加法。

这种优化方法减少了不必要的嵌套循环,使得每个颜色通道(如 RGB 和 A)的计算都能直接应用这些系数。这样,不仅提高了代码的可读性,还避免了复杂的循环操作。为了进一步优化,可以考虑将颜色与混合系数相乘,但这种方法并不会带来更多的优化,因为最终依然需要进行四次乘法。

总的来说,通过这种优化方式,能够将计算简化到只需要进行几次乘法和加法,大大提高了效率。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行游戏并注释代码

一切看起来都不错,虽然有点难以完全看清楚,但可以通过查看更近的部分来确认。目前的结果已经很不错了,所有的东西看起来都没有问题。接下来,为了更好地理解每个步骤,可以开始在代码中添加注释,比如标明“这是纹理着色器”,这样能帮助更清晰地看到每个操作的具体含义。

接下来,仍然需要处理一些部分,比如将 转换为线性并解包,同时还需要注意色彩的调制操作,这涉及到输入颜色和有效颜色范围的调整。这些步骤将有助于确保各部分的协调性。此外,还需要处理纹理的加载和相关的其他操作,确保流程的连贯性。

目前,流程已经趋于扁平化,能更清楚地看到每个阶段的处理。通过清理和简化代码,可以去掉一些不必要的步骤,确保每个操作都紧密相连,不再有多余的步骤或不必要的复杂性。最终的目标是能够完全理解每个阶段的功能,并检查是否有额外的操作是多余的。

扁平化 SRGB255ToLinear1

继续完成这部分工作时,需要处理 sRGB 到线性的转换,这涉及到解包每个纹理。对于这个过程,似乎没有太多优化的空间,毕竟它已经做得相当好。基本的操作是针对每个纹理进行处理,按顺序逐个解包,然后就地修改这些值。

不过,这个过程需要重复四次,因此要确保每次都正确处理。如果遇到任何错误(如代码中的小错误),就需要及时修正,确保每个步骤都能顺利执行。整体来说,这部分的工作是比较直接的,只要按步骤处理每个纹理,确保没有遗漏任何细节。
在这里插入图片描述

扁平化 Unpack4x8

最后的步骤是进行解包操作,整体看起来一切正常。解包的过程会将数据转换为浮动值,这样的操作能够正确处理纹理采样,转换为浮动格式后,结果会如预期那样进行处理。

在解包时,需要对每个纹理进行处理,这个过程需要重复执行多次,尤其是每次需要解包多个纹理的情况下,这就显得比较繁琐。虽然这个过程非常耗时,但它是造成程序运行缓慢的主要原因之一。如果代码量很大,并且操作的对象也很多,自然会导致程序执行变慢。

最终,通过这种方式完成所有的解包操作,虽然代码较为复杂,但它确保了每个步骤都得到了充分处理。这就是为何要处理这些繁琐的细节,以确保系统的正确运行。
在这里插入图片描述

所有内容已被扁平化

到此为止,所有的步骤都已经完成了。虽然过程比较复杂,但看起来并没有遇到什么无法解决的问题。虽然代码量较大,但其实并没有想象中的那么庞大。最终,这并不是最糟糕的情况,也算是可以接受的。

从编程角度来看,这个过程并不算是最困难或者最耗时的那种情况,相比之下还算是比较轻松的。尽管涉及到许多操作和细节,但总体来说,程序的复杂性并不至于让人感到过于沉重。
在这里插入图片描述

注意到代码运行更快

有些事情挺有趣的,虽然不太知道该怎么说,只能说,这就是常见的情况。大家会注意到,在编译时启用了优化选项(如 -O2),这意味着编译器会尽可能优化代码。实际上,这些优化措施并没有对程序性能产生很大的提升,唯一能够加速程序的可能是去掉那些冗余的代码,尤其是法线贴图的相关代码。

不过,实际上每次修改后,程序的执行速度都有所提高,尽管提升的幅度远没有预期的那么大。程序的优化并不是那么直接,尤其是对于一些刚性变换,编译器在优化这些代码时有一定的困难。编写优化过的代码其实是一项复杂的任务,编译器并不总能做到最好。

最终发现,如果在代码结构上做得更清晰,去除一些本可以由编译器处理的冗余项,结果往往是有益的。虽然大部分的优化工作编译器是可以自己完成的,除了我们做了 uv转置 变换和去掉一些冗余代码这两个操作,其他的优化基本上编译器自己就能做到。这也说明,程序的性能提升并不仅仅依赖于直接的优化,有时只是通过结构调整和去除无效部分就能获得好效果。

解包过程中出现了一个棘手的问题

接下来的重点是,我们需要对这段代码的执行时间做出一些预测,尽量找出它运行所需要的时间。这个估算有些困难,尤其是涉及到打包和解包操作。因为当我们实际去做优化时,打包和解包的表现会有所不同,特别是在SIMD版本中。

例如,像乘法操作在SIMD中看起来和普通的乘法一样,我们可以通过查看代码,判断是否需要执行乘法,如果需要,那么它就会变成一个简单的乘法操作进行优化。但是一些操作,比如打包和解包,实际表现差别较大,这使得它们的优化变得有些复杂。

这是我们需要讨论的一个更复杂的问题,涉及到代码如何处理以及如何进行符号化。具体来说,问题出现在解包操作上,因为解包需要先加载一个值,比如加载一个整型值,然后将这个值转换成多个通道的不同数据。这种转换操作需要从同一个值中读取多个地方,并进行不同的转换,这会使得解包操作相较于其他常规操作更为复杂。

其他的操作,如常规的标量操作,可以轻松地扩展到一次处理四个像素的数据,这不需要特别的优化。可是解包操作就不一样了,它需要做更多的处理和转换,导致在优化时面临更大的挑战。这就是我们当前所面临的一个难题,可能需要在接下来的讨论中进一步深入探讨。

Blackboard: 我们的“宽”策略是什么?

在讨论优化策略时,重点讲到了“SIMD”架构,即单指令多数据(SIMD)。这种架构的核心思想是一次性处理多个数据。例如,处理四个浮点值时,可能代表四个像素。具体来说,这些值可以映射到四个像素,或者映射到像素的RGB通道。这就引出了一个问题:如何选择要处理的四个值,以及这些值的排列方式。

使用RGB格式在SIMD架构中并不理想,因为大多数操作会分别处理Alpha通道和RGB通道,导致每次都需要处理Alpha通道时的不同方式,这样会降低效率,减少性能。比如,频繁的转换和处理Alpha通道会浪费大量的计算资源,导致性能下降。因此,最好采用一种能够使每次操作一致的方式。

一种更有效的方法是将数据按像素进行处理。在这种方式下,所有操作可以同时应用于四个像素的相同通道,这样操作一致性较高,效率也更好。但是,这样的优化在解包操作上仍然存在问题。

解包操作较为复杂,因为它不能简单地加载值并开始处理,而是需要将压缩的值解开。举个例子,假设有四个像素的RGB值被打包在32位中,四个像素可能像这样存储:RGBA,RGBA,RGBA,RGBA。这个时候,在解包时,必须分别提取每个像素的R、G、B和A通道,这个过程比较麻烦,因为需要分别将每个通道的值提取出来并重新组合。这种操作会浪费不少计算资源。

为了避免这个麻烦,可以在帧缓冲区格式上进行优化。例如,直接将帧缓冲区中的颜色通道按R、G、B、A分别存储,而不是每个像素按RGBA存储,这样可以减少解包操作的复杂度,提高效率。

为 SIMD 做准备

明天将讨论有关 SIMD 的内容,届时将进行详细讲解,重点是让大家理解 SIMD 的工作原理及其含义。计划通过黑板讲解的方式,帮助大家更清楚地掌握如何估算操作成本。主要目标是写一个类似 GPU 工作方式的软件,而不是追求高效的光栅化,因为在现代,若需要性能,使用 GPU 是更好的选择。

在优化过程中,不打算像 GPU 那样重新组织纹理和帧缓冲区,即不打算将它们按 R、G、B 通道打包。原因在于 GPU 有更强大的硬件支持来进行解包操作,因此它可以在不重新组织数据的情况下,通过硬件高效解包,并确保数据局部性(即 RGB 通道一起使用)。而我们则没有专门的硬件来进行此类操作,因此虽然性能会有所影响,但不做这些重组仍然能够取得较好的效果。

明天将讨论如何以相对较快的速度处理这些影响,尽管可能没有重新组织帧缓冲区那么高效,但会有合适的方式来弥补这个差距。

考虑固化纹理边界

今天的内容已经基本完成,接下来要做的就是确认是否有任何需要进一步明确的部分。一个可能需要稍微明确的是像素中心的概念,它决定了是否与像素相接触,以及如何将其转换为纹理坐标,这个部分可能需要更清晰和具体的说明。

除此之外,其他部分都已经过验证,代码的工作方式是正确的,已经能够执行预期的操作并生成正确的结果。因此,接下来的优化可以继续进行。之后可能会做一些小的修正,主要是为了确保在选择像素时的正确性,如果需要调整预处理部分以稍微改变像素的选择方式,这不是什么大问题。总的来说,现阶段的工作已经很接近完成。

目前代码写法中,你有非常长的依赖链(指令之间)。你会分解代码以去除它吗?

在这段内容中,讨论的是指令依赖关系和指令调度的问题。首先,明确表示代码中并不存在长时间的依赖链,尤其是在处理器(例如 Intel 处理器)上。具体来说,许多指令是独立的,可以并行执行,不受其他指令的影响。例如,四条指令可以完全独立地执行,彼此之间没有依赖关系。

尽管如此,指令之间仍然存在某些依赖性,例如在进行乘法运算时,可能需要等待某些指令的结果。不过,处理器有足够的指令窗口来处理这些延迟,即使在某些情况下,某些指令必须等到之前的指令完成后才能执行。比如,乘法指令可以并行执行,而平方操作可能需要等待先前的乘法结果。

如果担心处理器的执行效率,可以尝试重新安排指令顺序,将独立的指令一起推上去,或者将某些计算提取出来作为单独的操作。然而,在大多数情况下,这样的重排可能并不会显著提升性能,因为处理器的执行窗口已经足够大,能够有效处理这些独立指令的并行执行。

最后,尽管像像素处理这类任务确实存在某些串行依赖性(即需要按顺序处理),但现代处理器的处理速度非常快,乘法运算的延迟通常只有几个周期,因此对于大部分任务来说,依赖链对性能的影响较小。

为什么这流中使用 float 而不是 real32?

在这段内容中,提到了一些关于编程中的细节。首先,提到了一个原因,为什么在某个地方使用了 float 而不是 real32,虽然这个写法让人感到困扰,但实际上是有特定原因的。接着,提到在过去的三天里,花了大量时间进行优化工作,并且在这个过程中频繁使用了 float 这个关键字。因此,需要适应连续输入 float 这种习惯。
在这里插入图片描述

为什么使用 -O2 而不是 -O3 或 -Ofast(可能带有 -fverbose-asm)?

在这段内容中,讨论了关于编译器选项的问题。提到使用了 -O2 而不是 -Ofast,并表示自己对当前编译器的开关选项不是特别了解。接着,有人提出是否 -O3-O2 更快,并表示需要进一步测试。在实际的测试中,游戏运行时每个像素的处理大约需要 240 个周期。最后提到,编译器似乎忽略了未知的选项 -Ofast,并询问这些选项是否确实有效。

你是否使用过异或操作来避免管线停顿?如果没有,你使用什么方法?

提到并没有经常使用异或(exclusive or)操作来避免流水线停顿。尽管有时候会使用异或操作,但这并不是为了优化流水线停顿,而是因为在编程时需要进行一些测试和数据操作。优化的方式通常是通过直接使用SIMD(单指令多数据)来编写代码,进行适当的布局和安排,然后就完成了。这种方法并不涉及更复杂的优化技巧,例如通过异或操作来避免流水线停顿。

这些平方根操作不是很昂贵吗?

在这段内容中,讨论了平方根运算的效率。虽然平方根计算通常被认为是相对昂贵的操作,但实际上,现代处理器提供了相当高效的计算方式,尤其是对于倒数平方根(reciprocal square root)。倒数平方根运算的延迟非常低,通常只需要5个周期,甚至在某些情况下,可以在一个周期内完成多个倒数平方根计算,这使得它比常规的平方根计算更为高效。

常规平方根的计算也可以接受,尽管它的延迟较高,取决于处理器的不同,可能需要7到14个周期。然而,由于现代处理器的优化,平方根计算的开销并不像预期的那么大,仍然在可接受的范围内。如果需要提高渲染精度或效率,可以考虑使用倒数平方根来替代,或者在性能允许的情况下直接使用常规平方根计算。

总的来说,虽然平方根运算是较为昂贵的操作,但其执行时间在现代处理器上并不会对整体性能产生严重影响,特别是在通过并行处理和任务重叠的情况下,能有效地减少延迟带来的影响。
Intrinsics Guide
在这里插入图片描述

在这里插入图片描述

这两个是 Intel 的 SIMD 指令集(SSE 或 AVX)中的函数,用于对浮点数和整数进行向量化操作。

  1. __m128i _mm_cvtps_epi32(__m128 a)
    这个函数的作用是将一个 __m128 类型的浮点向量中的每个单精度浮点数(32位浮点数)转换为对应的整数(32位整数)。具体来说,它会将浮点数转换为整数,并将结果存储在一个 __m128i 类型的整数向量中。需要注意的是,浮点数会被截断(即,取整),而不是四舍五入。

    例如,如果 a 包含 [3.7, 2.5, -1.9, 4.3] 这样的浮点数,转换后会得到 [3, 2, -1, 4] 这样的整数向量。

    用途:常用于将浮点计算结果转换为整数,用于索引、计数、图像处理等场景。

  2. __m128 _mm_sqrt_ps(__m128 a)
    这个函数的作用是计算一个 __m128 类型浮点向量中每个浮点数的平方根。返回一个新的 __m128 类型向量,其中包含了原向量中每个元素的平方根值。_mm_sqrt_ps 是 “Square Root of Packed Single-Precision” 的缩写。

    例如,如果 a 包含 [4.0, 9.0, 16.0, 25.0] 这样的浮点数,调用该函数后将返回 [2.0, 3.0, 4.0, 5.0]

    用途:常用于图形学中的距离计算、物理模拟、图像处理等需要平方根的场景。

这两个函数都是使用 SIMD 指令来同时处理多个数据元素,极大提高了计算效率,特别是在需要处理大量数据时。

你会做多个 SIMD 后端吗?(SSE?/AVX/FMA 版本)

只会使用一个SIMD后端,因为其他的SIMD后端并不特别有趣。实际上,它们只是一些简单的变化。由于没有使用太多复杂的指令,所以这些后端的差异不大。尽管在解包操作时可能有一些可以不同处理的地方,但考虑到我们并不打算将渲染器作为最终产品发布,开发多个SIMD后端并没有太大意义。

你可以再加一层循环来提升一些变量的提升

可能确实可以在一个循环中去掉一些变量,但具体哪些变量可以去除还没有仔细查看。并不是很多,某些变量可以移动,编译器也肯定会优化这些操作。然而,之所以没有做这两项优化,是因为仍未决定乘法操作的具体实现方式。实际上,这里可能是错的,应该是减去1。

与其他工作负载相比,float<>int 转换的开销有多大?

浮点数转换的开销相比于工作负载的其他部分,通常只需要一个或两个周期。它是一个 SSC(SIMD 指令集)指令,具体来说,转换操作使用的是 cvttps2pi cvtps2dq 指令。在现代处理器上,这种转换操作已经变得非常高效,通常只需要一个周期就能完成。虽然过去这种操作比较昂贵,但现在它已经不再是瓶颈,执行起来非常轻松。

由于 xAxis 和 yAxis 通常是垂直的,我们是否应该为此做特殊处理?同样的,是否应为轴对齐的情况做特殊处理?

关于是否需要为 X 轴和 Y 轴的特殊情况做优化,认为其实没有太多需要特别处理的地方,因为每个像素的处理已经非常高效。尽管 X 轴和 Y 轴通常是垂直的,但在优化上,并不会带来显著的好处,特别是在每个像素都需要执行大量计算的情况下。

提到特殊处理对纹理查找可能会有帮助,特别是在可以重用已有纹理数据时,确实可能带来一些优化。然而,为了保持代码结构和 GPU 工作方式的一致性,不太倾向于深入这类优化。即便如此,若想追求极致性能,仍可以考虑对纹理访问做特殊优化,尤其是当数据是对齐的时,可以通过优化纹理获取过程来提高效率,特别是减少不必要的纹理加载。

对于对齐访问,可能会选择分别处理缩小和增大的情况,从而能够重复利用缓存中的纹理数据,但整体上,这类优化的收益并不显著,且实施起来复杂度较高,因此是否实施取决于具体的性能需求。

编译器会做任何自动的 SSE 优化吗(或者有相关选项吗)?

编译器是否自动进行 SSC 优化的问题,通常情况下编译器并不会自动做很多针对 SSC(SIMD 指令集)优化。尽管一些编译器,如 GCC,可能会做一些优化,但通常这些优化并不会像手动写代码时那样精确。在 Visual Studio 中,编译器可能不会进行类似的优化,除非显式地开启特定选项。

通过查看生成的汇编代码,可以发现编译器并没有利用 SSC 指令集进行并行操作。它通常会在寄存器的低位部分执行操作,而没有扩展到更高位,意味着它基本上是在逐个处理数据,未能利用 SIMD 寄存器的并行能力。因此,通常情况下,编译器默认并不会对 SSC 指令集进行优化,开发者需要手动编写代码来实现这些优化。

sqrt_ss vs sqrt_ps vs sqrt_pd?

sqrt_ss, sqrt_ps, 和 sqrt_pd 都是 SSE (Streaming SIMD Extensions) 指令集中的平方根计算指令,分别针对不同的数据类型。

  • sqrt_ss: 计算单精度浮点数(float)的平方根。它只会对一个单独的 float 数据执行平方根操作,并且会将结果存储到目标寄存器的第一个位置。

    • 操作数: 单个 float(单精度浮点数)。
    • 返回值: 目标寄存器的第一个元素。
  • sqrt_ps: 计算四个单精度浮点数(float)的平方根。它使用 SIMD 指令,一次性对四个 float 数据进行平方根计算。

    • 操作数: 一个包含四个 float 的寄存器(__m128 类型)。
    • 返回值: 返回一个包含四个平方根结果的寄存器。
  • sqrt_pd: 计算两个双精度浮点数(double)的平方根。它类似于 sqrt_ps,但是针对的是双精度浮点数。

    • 操作数: 一个包含两个 double 的寄存器(__m128d 类型)。
    • 返回值: 返回一个包含两个平方根结果的寄存器。

简而言之:

  • sqrt_ss 处理单个 float
  • sqrt_ps 同时处理四个 float
  • sqrt_pd 同时处理两个 double

这些指令使得在进行向量化计算时,可以高效地并行处理多个数据元素,减少处理时间。

SSE 是否允许使用指数 2.2 进行 sRGB 计算,而不是使用 2 来近似,且不会造成很大的性能损失?

在讨论使用指数运算代替近似计算sRGB时,提出了一个观点,即使用指数运算(例如 2^x)可能会更复杂,尤其是当需要通过表格查找时,这会增加额外的复杂性。与直接进行平方根或平方的运算相比,使用 2^x1/2^x 来进行近似并不容易,而且会增加计算难度。尽管这种方法可能不会显著影响性能,但确实会带来与直接运算不同的表现,尤其是在处理时需要使用表格查找,操作变得更复杂和繁琐。

简单来说,平方和平方根的运算比进行指数运算(例如 2^x)要简单得多。尽管指数运算在某些情况下可以提供精确的结果,但它所带来的计算成本和复杂性要比平方根和平方运算更高。

你没有自动 SIMD 的主要原因是精确的异常处理。你可能需要告诉编译器你不需要它们

讨论中提到,自动向量化的主要原因之一是编译器需要精确的异常处理,可能需要明确告诉编译器不需要这些精确的异常处理。某些浮点操作需要遵循特定的标准和规范,编译器必须遵守这些规定才能保证正确的计算行为。然而,如果不允许编译器有一些灵活性,它就无法进行优化,从而影响性能。

有一些标志可以控制这些行为,通常这些标志用于告知编译器是否需要精确的异常处理或浮点一致性。精确异常处理涉及到对浮点计算中的异常进行严格管理,而浮点一致性则涉及到操作顺序的约束。通过允许编译器在这些方面有更多的自由度,可以让它执行更多的优化,从而提高性能。

尽管有一些标志(如 -fp-fast)可以用于尝试让编译器变得宽松,但仍然不确定这些标志是否会显著改善性能。即使启用了这些标志,也可能并不会立刻看到向量化效果。通过设置断点查看,仍然无法确认是否实现了预期的优化。因此,尽管可以尝试这些设置,但并不确定它们是否会带来太大的帮助。

如果启用了 “/arch:AVX2” 开关,会发生什么?

如果启用某些特定的硬件功能(如 AVX 或其他 SIMD 指令集)并尝试在不支持这些功能的机器上运行,可能会导致程序崩溃。例如,如果机器不支持 AVX 指令集或其他相关的硬件扩展,而程序尝试使用这些指令,系统就会无法执行这些指令,导致崩溃。具体来说,如果机器不支持 AVX2 或更高版本的 SIMD 指令集(如 SSE4.2),即使程序可能包含这些指令,它也会因为硬件不兼容而发生崩溃或无法正常运行。

看看这些 AVX-512 的内容

最近遇到了一些令人困惑的情况,看到一些硬件功能和指令集的分类方式非常复杂,以至于无法理解其含义。通常,当查看指令集时,可以直接看到指令的分组,但这次看到的一些分类方式让人迷惑,甚至不知道这些子类别具体指的是什么。例如,看到了一些如“AVX-512”之类的术语,不知道这些是什么意思,也不明白为什么会有这样的分类。

有一种可能的解释是,这些是处理器的不同功能或指令集,甚至可能涉及到处理器的某些硬件位,这种细分让人觉得很混乱。如果真的是这样,那就太复杂了,因为在这种情况下,需要非常仔细地检查每种组合的可用性,确保正确执行。这种分类方式如果是实际存在的,会显得过于复杂,甚至可能让人觉得无法管理。希望这并非事实,因为如果真是如此,那将是一个非常复杂的系统。

AVX-512(Advanced Vector Extensions 512-bit)是一种指令集扩展,属于Intel的处理器架构。它是AVX系列的一部分,专为高性能计算和并行处理设计。AVX-512允许处理器在每个时钟周期内处理更多的数据,提高了处理器在处理浮点运算、整数运算和加密任务时的效率。

AVX-512 提供了512位的宽度,可以在一个指令中并行处理更大的数据集,相比之前的AVX和AVX2(支持256位的向量),AVX-512在数据处理方面有更强的能力。具体来说,AVX-512的特点包括:

  1. 更高的并行度:每条指令可以处理更多的浮点数或整数,从而提高计算密集型任务的性能。
  2. 更宽的寄存器:AVX-512使用的寄存器宽度为512位,相较于AVX的256位寄存器,可以同时处理更多的数据。
  3. 更多的指令:AVX-512包含了许多新的指令,支持更多类型的操作,包括加密、科学计算和机器学习等领域。

使用AVX-512指令集可以显著提高对某些应用程序(如科学计算、图像处理、加密等)的处理能力,但需要注意的是,它对硬件的要求较高,只有支持AVX-512的处理器才能充分发挥其性能。

NTRinsics Guide 是一个指令集的参考手册,帮助开发者了解和使用各种处理器的低级指令,尤其是关于Intel和AMD处理器架构的指令集。

在这里插入图片描述

FMA 是融合乘加

讨论中提到,使用乘法加法(Multiply-Add)指令时,可能会遇到一些复杂的硬件标志和指令集,尤其是在启用 AVX-512 时。对于一些特定的硬件功能和选项(如“FM”指令集),并不完全了解它们的具体含义。虽然“FM”看起来与乘法加法相关,但“FM”本身是一个单独的功能,可能是可选的,不一定与硬件直接相关。

另外,对于一些看起来是硬件标志或功能的术语,如“DQ”,并不清楚它们的具体意思。也不确定这些术语是否表示处理器的标志位,如果是的话,可能需要查看处理器的文档来确认这些标志是否支持。在某些情况下,可能需要启用某些功能来执行特定操作,而这取决于处理器是否支持这些功能。

此外,文中提到的 AVX-512 指令集目前还没有完全普及,所以对于它的研究还在进行中。由于对这些功能和术语的理解还不完全,可能需要查阅架构参考文档来弄清楚它们的具体意义。因此,当前对这些标志和功能的理解仍然存在很多不确定性。

AVX-512DQAVX-512 指令集的一部分,专门用于处理 512 位向量数据。它的“DQ”代表“Double Quadword”,指的是在 512 位的寄存器中处理双 64 位数据(即双精度整数或浮点数)。

具体来说,AVX-512DQ 提供了对 双 64 位整数双精度浮点数 运算的支持。这个扩展支持多个浮点和整数操作,如加法、减法、乘法等,并且通过 512 位宽度的寄存器,可以在一个时钟周期内处理更多的数据,提高计算密集型任务的性能。

简而言之,AVX-512DQ 主要关注的是在处理 64 位整数和双精度浮点数时,能够利用 AVX-512 提供的更高并行度和性能。

是的,看起来是不同的硬件能力位

讨论中提到,存在不同的硬件标志位(caps bits),这些标志位可能与科学计算相关,可能会导致处理器在不同的条件下选择不同的路径。如果处理器需要根据这些标志位随机组合来切换路径,可能会带来极大的复杂性和性能问题。这种做法被认为不合理,因为对于商业产品来说,使用这种复杂的标志位组合是不现实的,尤其是在实际应用中,很少会需要依赖这种灵活的路径切换。使用这种方式可能会导致不必要的复杂性,且可能在实际产品中不具备可行性,因而被认为是不可取的。
在这里插入图片描述


http://www.ppmy.cn/ops/160252.html

相关文章

深入理解ArkUI:自定义组件与高效数据管理

深入理解ArkUI&#xff1a;自定义组件与高效数据管理 在第一章中&#xff0c;我们初步探索了HarmonyOS的UI开发框架ArkUI&#xff0c;从声明式语法到组件化开发&#xff0c;再到动画系统和多设备适配&#xff0c;ArkUI以其独特的设计理念为开发者提供了丰富的工具和手段。本章…

支持向量机(SVM)在 NLP 中的使用场景

支持向量机(Support Vector Machine, SVM)是一种强大的监督学习算法,广泛应用于分类任务中。由于其出色的分类性能和高效的计算特点,SVM 已经成为自然语言处理(NLP)领域中的一种经典模型。SVM 在 NLP 中的应用非常广泛,尤其在文本分类任务中,表现出色。 本文将探讨 SV…

C++ 课程设计 汇总(含源码)

C 课程设计 [C课程设计 个人账务管理系统(含源码)](https://arv000.blog.csdn.net/article/details/145601695)[C课程设计 运动会分数统计&#xff08;含源码&#xff09;](https://arv000.blog.csdn.net/article/details/145601819)[C 课程设计打印万年历&#xff08;含源码&a…

阿里云子账号管理ECS权限配置全指南

阿里云子账号管理ECS权限配置全指南 ——主账号授权三步走&#xff0c;附精细化权限管控方案 一、基础版&#xff1a;授予子账号ECS全量管理权限 Step1&#xff1a;主账号登录RAM控制台 进入阿里云控制台 → 顶部导航栏点击头像 → 选择访问控制(RAM)4。左侧菜单选择用户 → …

理解都远正态分布中指数项的精度矩阵(协方差逆矩阵)

之前一直不是很理解这个公式为什么用这个精度矩阵&#xff0c;为什么这么巧合&#xff0c;为什么是它&#xff0c;百思不得其解&#xff0c;最近有了一些新的理解&#xff1a; 1. 这个精度矩阵相对公平合理的用统一的方式衡量了变量间的关系&#xff0c;但是如果是公平合理的衡…

使用 Python 爬虫和 FFmpeg 爬取 B 站高清视频

以下是一个完整的 Python 爬虫代码示例&#xff0c;用于爬取 B 站视频并使用 FFmpeg 合成高清视频。 1. 准备工作 确保安装了以下 Python 库和工具&#xff1a; bash复制 pip install requests moviepy2. 爬取视频和音频文件 B 站的视频和音频文件通常是分开存储的&#x…

边缘计算网关与 PLC:注塑机车间数据互联新变革

在当今数字化浪潮席卷而来的时代&#xff0c;制造业的智能化转型成为了提升竞争力的关键路径。对于注塑机车间而言&#xff0c;如何实现数据的高效采集与互联&#xff0c;进而优化生产流程、提高生产效率&#xff0c;是众多企业亟待解决的问题。而明达MBox20边缘计算网关与 PLC…

【Axure高保真原型】拖动画图——画矩形案例

今天和大家分享拖动画图——画矩形案例的原型模板&#xff0c;我们可以在指定区域通过拖动的方式画出矩形&#xff0c;可以画出多个矩形&#xff0c;矩形样式也可以自行定义。使用也很方便&#xff0c;复制粘贴元件组&#xff0c;然后调整画图区域的尺寸&#xff0c;即可自动生…