游戏引擎学习第118天

打印 上一主题 下一主题

主题 900|帖子 900|积分 2700

堆栈:https://gitee.com/mrxiao_com/2d_game_3
优化工作概述

这次我们正在进行一些非常有趣的工作,主要是对游戏进行优化。这是首次进行优化,我们正在将一个常规的标量C代码例程转换为内建指令,以便利用AIX 64位处置惩罚器的SIMD指令集进行加速。到目前为止,已经通过这些基本的转换工作实现了3倍的性能提升。为了夸大这一点,即使在开始深入优化之前,仅仅通过这种方式翻译代码,也能带来显著的性能提升。
今天我们将继续完成这一转换过程,只管在今天完成这一工作。
回顾昨天的进展

昨天的工作中,处置惩罚到了一个非常详细的题目,我们在当时正准备解决。今天我们将从前次停下的地方继续推进。
目前,在运行步伐时,固然通过了全部的单元测试,但在显示内容时,依然出现了玄色的条纹。题目的缘故原由正是这些玄色条纹的出现。

当前题目:黑条

为了进行一些工作,避免修改像素写入的方式,原本用于检查是否应添补的代码被注释掉了。在之前的实现中,每次处置惩罚像素时,都会设置一个名为 ShouldFill 的布尔数组,表现该像素是否在需要添补的矩形地区内。
但由于当时还没有处置惩罚如何在 SIMD 中实现这一点,所以这部门代码被注释掉了。当前的使命是,只在 ShouldFill 为 true 的像素上进行写入操作。然而,步伐运行时,未能进行这种检查,导致一些玄色的像素被写入,这些本不应该被写入。
题目出在 ShouldFill 没有被准确设置。在上面的代码中,如果 ShouldFill 为 false,就会跳过相应的操作,效果是缓冲区里没有数据,终极写出的就是无效的垃圾数据。因此,需要接纳一些方法来保存 ShouldFill 的信息,确保只有需要添补的像素被准确写入。

黑板:将准确的值写入目的

这个过程其实并不复杂,但因为这是全新的内容,之前在 game Hero 中并没有讨论过雷同的内容,所以需要确保每个人都明确当前的进度。昨天的工作进展是,已经准备了一个 128 位的 SIMD 寄存器,其中存储了 RGB 和 A 四元组的数据。
目前的题目是,固然我们已经准备好了一次性写入到目的缓冲区的指令,但目的缓冲区可能并不包罗全部应该覆盖的值。详细来说,目的缓冲区中可能只有一些像素值是我们需要更新的。
为相识决这个题目,可以接纳的一种方式是,在处置惩罚时确保,如果只需要更新两个像素,那么其他两个像素可以添补为有用数据(也就是目的缓冲区原本的数据)。这样,即使我们写入了这两个像素的数据,也不会影响其他像素,因为它们将被写回原来的值。
这种做法是我们首先要考虑的方案,然后可以根据实际情况判断是否有其他更好的方法。在这种情况下,关键是要确保当计算新的值时,固然不更新其他像素,但我们依然将原始帧缓冲区中的旧值保留在那些未处置惩罚的像素位置,从而确保终极的效果是准确的。
对全部像素进行全部操作是可以的

一种思考方式是,我们盼望避免对那些不需要处置惩罚的像素重复进行全部操作。固然目前在处置惩罚每个像素时,处置惩罚一个像素的代价和处置惩罚四个像素的代价差不多,但如果实验将来自不同位置的像素打包并在之后再分散回来,这可能对于四个像向来说并不值得实验,只管很难说。出于这个缘故原由,暂时并不会实验这种方法。
因此,目前的做法是,完全可以对全部像素进行这些操作。唯一的题目是,若对全部像素实验这些操作,到最后我们必须找到一种方法,将全部这些计算得到的值准确地添补回原本在目的缓冲区中的像素值。这意味着需要对全部处置惩罚过的值进行操作,确保它们终极能够准确地复现目的缓冲区中的原始内容,但这好像有点麻烦,甚至感觉有些难度。
另一种选择是,我们可以考虑其他方法来简化这一过程。
黑板:另一种选择:组合旧值/新值

在我们准备写出新值之前,可以考虑使用按位操作来联合这两个值。假设我们已经计算出了四个槽的新的值,并且知道只有两个值是有用的,其他的应该保留旧值。那么,可以使用按位操作将这两个值联合,得到一个终极的向量,其中旧值出现在应为旧值的位置,新值则出现在应为新值的位置。
为了实现这一点,可以使用“或”操作将两个向量联合。如果我们有两个向量,其中包罗全部的零,我们可以将它们进行按位“或”运算,这样可以确保中心的位置保持新值,而两端则保留旧值,因为零不会干扰。
接下来的题目是,如何将不需要的新值变为零。在这种情况下,可以使用“与”操作来实现。如果构建一个掩码,将其应用到按位“与”操作中,就可以打扫不需要的部门,确保终极的向量只有我们需要的值。
黑板:构建掩码

这看起来像是这样:我们可以构建一个掩码,其中全部应该是旧值的位置都是零,全部应该是新值的位置都是一。通过使用“与”操尴尬刁难掩码和旧值进行操作,可以打扫不需要的部门,然后将效果与新值进行“或”操作,得到终极的向量。
这种方法的关键在于,确保能生成一个准确的掩码,使得掩码的值在需要旧值的位置为零,在需要新值的位置为一。这样就能非常简朴地将新值和旧值联合成一个终极的效果。
掩蔽无效的新值

可以创建一个名为“MaskOut”的方法,用于通过“与”操作和掩码生成终极效果。首先,使用掩码将原始目的值中的不需要覆盖的部门打扫,然后将新计算的像素值中的不需要写入的部门也打扫。之后,通过“或”操作将这两个部门归并,得到终极的目的值,最后将其写入目的位置。
为了实现这一过程,需要确保能够准确地计算掩码,并存储原始目的值。这意味着在进行更新操作之前,首先要处置惩罚原始数据,并确保每次操作都按照准确的次序实验。

确保保存原始目的

需要确保每次都能准确处置惩罚目的数据,即使是在循环条件中,也不能仅仅在特定条件下进行加载。必须始终加载并更新目的数据。完成这一点后,可以确保在处置惩罚过程中原始目的数据能够得到保留,并且新计算的值能够准确地覆盖原有数据。
此外,还需要注意,目前这些数据仍然是直接转换为浮动格式的,并没有进行SIMD化处置惩罚。因此,在此过程中,除了处置惩罚掩码操作外,还需要对数据格式进行相应的调整。
还没有对加载进行SIMD化,分别处置惩罚OriginalDest

为了确保在处置惩罚过程中能够准确保留原始目的数据,需要显式加载目的位置的数据。这可以通过将目的数据加载到特定寄存器中实现,雷同于之前的操作,只不外这次是加载正在写入的特定位置的值。这样做的目的是能够保存之前的数据,并在后续处置惩罚中使用。
目前,固然数据还没有进行SIMD化处置惩罚,但这并不影响目前的操作。此时,将继续保持加载操作的方式,待以后完成SIMD化后,代码可以更新为使用该格式。

WriteMask题目:还没有计算它!

目前,我们面对着一个题目,即写掩码(WriteMask)尚未显式计算。为相识决这个题目,暂时可以像之前处置惩罚其他题目一样,使用一个简化的方式来进行近似操作。我们可以将右侧掩码设置为一个假造值,比如将其值设为零,意味着不会实验任何写操作。通过这种方式,验证可以看到,确实没有写入任何内容。
如果将右侧掩码设置为全1(即每个32位的掩码都是1),则会规复到之前的状态,即在全部地方都实验写操作,这是预期的行为。
接下来,需要解决的题目是如何将右侧掩码设置为准确的值。可以通过雷同的方式,在后续的代码中使用简化的宏来进行设置。

使用简朴的set宏来设置WriteMask

目前,直到找到更好的方式解决这个题目,我们决定将右侧掩码设置为全1。在这种情况下,只有在我们以为需要添补的地方,右侧掩码才会被设置为1。这意味着,只有在需要写入的地方,掩码才会有用,其他地方则不会进行写操作。

game Hero: 一个有点花哨的版本

出现了一个不测的效果,看起来像是某种艺术滤镜被应用到了游戏中,效果非常独特,甚至有些浮夸。这个不测的效果让人感到非常惊讶,但也觉得很有趣。现在,好像是由于使用了浮动点设置所导致的,因此需要暂时调整一下,使用整数来处置惩罚这个题目。

修复“题目”:为uint设置的Mi宏

这个效果固然不测,但确实很酷。现在,通过准确的掩码操作,已经能够正常掩藏掉不需要的值,效果看起来很好,之前的玄色边框题目也解决了。这标志着又一个使命的完成。接下来,还需要提到另外一个题目。


另一个题目:Fabian的舍入模式注释

颠末确认,实际上不需要担心广告操作,因为默认的四舍五入模式已经是“最接近值”,因此不需要额外处置惩罚。这样就不需要再实验之前的步调,可以省去这个使命,进而简化了代码。现在,系统运行得更加高效,已经达到了约莫110个周期的速度。

还有一些工作要做,最后一个for(I)循环

目前,剩下的工作是完成最后的四个循环。接下来的目的是将一些操作,比如AR、GS和BE的打包息争包,移到外部,利用SIMD实现这一过程。这些操作之前已经做过雷同的处置惩罚,所以下移这些操作并不会很困难。首先,决定将样本A移到外部,并将其存储在M128寄存器的低位部门,而不是直接作为UN32加载。这个过程将使得我们能够更机动地处置惩罚数据,固然还不确定是否需要使用strided存储,或者继续使用UN32进行加载。无论如何,四个样本A的处置惩罚应该在外部完成,首先进行显式加载。
显式版本的循环睁开

在这一阶段,决定采用徐徐的方式处置惩罚循环,而不是一次性处置惩罚全部样本。这样做会生成大量的转换代码,但可以相对简朴地完成全部样本的处置惩罚。全部样本将按照这种方式处置惩罚,每个样本可以独立进行转换。为了方便展示,将这些步调逐一展示,使得每个细节都能被清晰看到。未来在处置惩罚更复杂的汇编使命时,可以根据需要做一些简化,避免一次性跳进复杂的SIMD代码编写,徐徐展示每个步调的好处是让不同砚习进度的人都能跟上。
该部门的代码处置惩罚涉及从打包纹理中加载数据,这使得操作稍显复杂。只管一开始看起来直接处置惩罚会更简洁,但由于纹理格式的不同,实际加载数据时会遇到一些难度,因此此时的处置惩罚方式需要更加谨慎。


检查我们是否还在正常工作:现在每像素不到100个周期

如前所述,在移动某些操作后,循环计数反而镌汰了,很多情况下已降到低于一百个周期。这看似与直觉相反,因为有些代码从条件语句中移到了常规实验路径中,这意味着它不再仅在条件满足时实验,而是始终实验。只管如此,处置惩罚器和编译器的优化方式以及实验机制偶然会导致一些不测的效果,特别是在处置惩罚复杂的例程时,这种优化效果可能会出乎意料。因此,只管这种变革好像不太直观,但它的效果是显着的。
接下来,考虑目的部门的处置惩罚,这可能是最轻易开始的部门。已经加载了原始的数据,可以开始着手处置惩罚目的部门,开端考虑只处置惩罚低位目的,或者先从右掩码着手。
以相同方式处置惩罚目的

接下来,继续进行下一步处置惩罚,完成当前使命。将更多的步调移到外部实验,确保过程顺利进行。

移动数据后节流了更多周期

通过将代码移出条件语句,节流了约莫十个周期。只管还没有完全翻译例程,但已经接近目的,周期数已接近五十个。接下来,打算首先处置惩罚右掩码的题目。
修复WriteMask的乱象

计算u和v值后,需要确保它们满足某些束缚条件,即它们必须大于等于0并且小于等于1。目的是直接计算右掩码,而无需进行标量提取,因为当前代码通过循环从向量中提取各个部门。为了避免这种操作,计划通过使用SSC指令来计算右掩码,从而不需要将其打包到四个单独的通道中。
SSE比较操作

SSC提供了多种比较操作,包罗“greater than or equal to”(大于或等于)等指令。这些指令可以比较两个值,如果满足条件,效果会在对应的通道中添补全1,否则添补全0。该操作专门设计用来处置惩罚此类比较需求。

黑板:宽操作的比较

使用 SSC 提供的比较操作,可以直接生成右侧掩码。该操作比较每个通道的值,并根据条件生成全 1 或全 0 的掩码。这种机制是为了避免在宽操作中使用条件控制流,因为在宽操作中无法仅部门实验某些操作,必须对整个向量进行处置惩罚。通过选择性地应用条件判断,可以计算出每个像素的终极值。这个过程本质上是在计算一个条件语句的两个分支,并在每个通道中根据条件选择实验哪个分支。
emmintrin 是一个包罗 128 位 SSE 指令集的内建函数库,用于在多个数据元素上同时实验相同的操作,特别是整数类型的数据。下面是一些常见的 emmintrin 比较操作及其应用示例。
常见的 emmintrin 比较操作


  • _mm_cmpgt_epi32: 比较两个 128 位的整数向量中每个元素是否大于,生成一个掩码效果。
    1. __m128i _mm_cmpgt_epi32(__m128i a, __m128i b);
    复制代码

    • 该操作返回一个 128 位的整数向量,每个元素都是 0 或 0xFFFFFFFF(即全 1),表现对应元素是否满足 a > b 的条件。

  • _mm_cmpeq_epi32: 比较两个 128 位的整数向量中每个元素是否相等,生成一个掩码效果。
    1. __m128i _mm_cmpeq_epi32(__m128i a, __m128i b);
    复制代码

    • 返回一个 128 位的整数向量,每个元素如果对应位置相等,效果为 0xFFFFFFFF,否则为 0。

  • _mm_max_epi32: 对两个 128 位整数向量中的每一对元素,取较大的值。
    1. __m128i _mm_max_epi32(__m128i a, __m128i b);
    复制代码

    • 返回一个新的 128 位整数向量,其中的每个元素为对应位置上两个输入向量的最大值。

  • _mm_min_epi32: 对两个 128 位整数向量中的每一对元素,取较小的值。
    1. __m128i _mm_min_epi32(__m128i a, __m128i b);
    复制代码

    • 返回一个新的 128 位整数向量,其中的每个元素为对应位置上两个输入向量的最小值。

  • _mm_and_si128: 对两个 128 位整数向量的元素进行按位与操作,用于生成掩码。
    1. __m128i _mm_and_si128(__m128i a, __m128i b);
    复制代码

    • 对两个向量的每一位实验按位与,返回一个新的向量。

应用举例

1. 条件选择

假设需要对两个数据会合的数值实验条件选择操作,比如根据某个条件选择哪个数据会合的数值进行处置惩罚。可以利用 emmintrin 提供的比较操作和掩码操作来高效实现。
示例:
  1. #include <emmintrin.h>
  2. #include <stdio.h>
  3. int main() {
  4.     // 定义两个 128 位整数向量
  5.     __m128i v1 = _mm_set_epi32(10, 20, 30, 40);
  6.     __m128i v2 = _mm_set_epi32(5, 25, 35, 45);
  7.     // 使用 _mm_cmpgt_epi32 比较 v1 和 v2
  8.     __m128i mask = _mm_cmpgt_epi32(v1, v2);  // v1 > v2 时为 0xFFFFFFFF, 否则为 0
  9.     // 使用 _mm_and_si128 和 _mm_set_epi32 进行条件选择
  10.     __m128i result = _mm_and_si128(mask, v1);  // 如果 mask 为 0xFFFFFFFF,则选择 v1 中的值,否则选择 0
  11.     // 打印结果
  12.     int* resultArray = (int*)&result;
  13.     for (int i = 0; i < 4; i++) {
  14.         printf("%d ", resultArray[i]);
  15.     }
  16.     return 0;
  17. }
复制代码


表明:

  • 使用 _mm_cmpgt_epi32(v1, v2) 比较向量 v1 和 v2 中的每一对元素,生成一个掩码 mask,如果 v1 的元素大于 v2 的元素,则掩码对应位置为 0xFFFFFFFF,否则为 0。
  • 使用 _mm_and_si128(mask, v1) 仅选择掩码位置为 0xFFFFFFFF 的元素,其他位置将为 0。
输出:
  1. 0 0 0 10
复制代码
2. 图像处置惩罚

在图像处置惩罚中,可以使用 emmintrin 的比较操作快速进行像素值的比较与操作,生成掩码并基于条件处置惩罚图像像素。
示例:
假设需要对图像中每个像素的亮度值进行比较,并根据某个条件修改亮度。
  1. #include <emmintrin.h>
  2. #include <stdio.h>
  3. int main() {
  4.     // 模拟两幅图像的像素值
  5.     __m128i image1 = _mm_set_epi32(100, 150, 200, 250);
  6.     __m128i image2 = _mm_set_epi32(50, 180, 190, 230);
  7.     // 比较图像中的像素值,找出较大的像素值
  8.     __m128i result = _mm_max_epi16(image1, image2);
  9.     // 打印结果
  10.     int* resultArray = (int*)&result;
  11.     for (int i = 0; i < 4; i++) {
  12.         printf("%d ", resultArray[i]);
  13.     }
  14.     return 0;
  15. }
复制代码

表明:


  • 使用 _mm_max_epi16(image1, image2) 来选择 image1 和 image2 中每对像素的最大值。这样可以有用地对图像像素进行比较并选择较亮的像素。
输出:
  1. 100 180 200 250
复制代码
总结

emmintrin 的比较操作提供了高效的向量化处置惩罚能力,使得可以在 128 位的多个数据元素上同时实验条件判断操作,从而在图像处置惩罚、物理模拟和其他计算麋集型应用中大幅提升性能。通过使用这些指令,可以更高效地处置惩罚条件控制、掩码生成、最大/最小值选择等操作,镌汰冗余计算,提高代码实验效率。
使用比较直接生成WriteMask

在这种情况下,条件语句无法使用,需要通过掩码操作来实现。因此,使用了特定的掩码操作来替代传统的条件判断。步调如下:

  • 掩码生成:将比较操作的效果(如大于或等于的效果)直策应用于掩码中。这意味着不再依赖于传统的条件分支,而是使用掩码值来控制哪些部门会实验操作。
  • 使用零和一的向量:已经加载了零向量和一向量,然后通过比较操作(比方大于或等于零、小于或等于一)来生成掩码。这些掩码决定了哪些部门的数据会被使用或跳过。
  • 使用 SSE 指令:通过利用 SSE 指令集(比方 ssc 指令),可以在不使用传统条件判断的情况下,直接生成这些掩码。这样可以高效地进行并行计算。
  • 类型转换:由于操作涉及浮点数,需要在计算完成后将掩码进行类型转换,以确保效果准确并与目的类型匹配。终极,生成的掩码是基于浮点数运算的,可能需要转换为得当的整数类型或其他格式。
通过这种方式,避免了传统的条件判断,改为使用掩码来控制数据流,优化了运算效率。

使用宽操作的工作WriteMask

现在,通过使用操作,完全生成了右掩码,并且运行得非常顺利。这表明,掩码生成的过程是有用的。然而,题目在于,只管右掩码已成功生成,计算部门仍然存在,因为仍然需要加载纹理。
题目:无法完全去掉if语句…

现在存在一个题目,就是不能完全去除条件语句并始终加载纹理。缘故原由在于,可能会从无效的内存中加载数据。必须确保始终加载有用的值,否则可能会访问到无效的内存。因此,需要在加载时进行有用性检查,确保数据的正当性。
解决方案:限定U和V

为了确保全部的纹理加载都能准确实验,即使在纹理未实际使用的情况下,依然需要对u和v值进行限定(clamp),确保它们始终保持在有用范围内。这样做可以避免加载无效的纹理数据。通过将u和v值限定在0到1之间,任何超出范围的值都已经被处置惩罚并映射为无效。因此,通过这种方式可以确保全部需要的像素都被准确计算并写入,而不受范围外的影响。

完全去掉if语句!

通过提前对u和v值进行限定(clamp),可以完全避免在后续操作中选择超出纹理范围的像素。这样一来,就可以确保不会访问无效的纹理地区,并且在进行纹理采样时,全部的像素都会处于有用范围内。

也将纹理获取宽化

通过提前计算纹理的宽度和高度,可以将这些计算步调变成符号操作。这样,纹理的相干计算将以宽向量的形式进行,这对于后续的纹理坐标处置惩罚黑白常方便的。这些计算会通过乘法操作进行,可以确保每个像素值都实用于相同的处置惩罚。同时,也需要在后续步调中调整这些计算,特别是在处置惩罚纹理边界时。

还没有进行优化,只是在转换为SIMD

为了优化纹理计算,计划将原本逐个像素进行的标量操作改为通过宽向量进行的批量处置惩罚。通过将 U 向量与宽度向量相乘,可以一次性处置惩罚全部像素,从而避免逐一计算。这样做不仅提升了效率,也简化了代码布局。同时,考虑到纹理的提取和计算过程,计划对 x 和 y 的提取方式进行改进,确保操作更为直接和清晰。对于一些命名不规范的变量,建议进行优化和重命名,以提高代码的可读性和易维护性。

调整纹理获取以使用宽值

为了进一步优化纹理计算,计划引入一个新的转换操作,用于从之前计算中提取相干的值。这将通过提取像素的 x 和 y 坐标,确保能够有用地从宽向量中提取纹理数据。通过这种方式,能确保计算更为高效,并镌汰重复的操作。
同时,需要注意的是,_mm_cvtps_epi32 操作在实验时可能会引发一些题目,因为它的行为和数据转换方式需要特别关注,以确保不会影响到后续的计算过程。总的来说,这一改进的目的是通过简化和优化纹理提取流程,提高性能,并使代码更加简洁明了。
通过截断转换获取坐标

为了确保在计算过程中能够准确地处置惩罚纹理坐标,需要先对值进行截断,而不是四舍五入。为了实现这一点,可以通过先减去 0.5 来确保数值能够落入准确的范围,这样就能达到截断的效果。固然最初以为无法直接进行截断操作,但通过使用 _mm_cvttps_epi32 指令,实际上可以实现这一功能,而不需要额外的减法操作,这为代码的简化提供了便利。
在完成乘法计算后,将生成纹理的 tx 坐标,然后使用 _mm_cvttps_epi32 来截断该值,进而得到准确的纹理像素索引。这一过程的关键是准确计算纹理的位置,并确保从准确的像素数据中提取信息。这种方法能有用避免毛病,并确保纹理映射过程的准确性。

通过减法获取fX和fY

为了处置惩罚纹理坐标的分数部门,需要计算 x 和 y 的小数部门,这可以通过减去其截断后的整数值来实现。这一操作会得到纹理坐标的准确小数部门,从而用于准确的纹理采样。
在这个过程中,需要将整数转换为浮动点数,因此需要使用一种将整数向上转换为浮动点数的指令(比方 ep32),而不是进行简朴的类型转换。通过这种转换,能够保持精度,并确保纹理坐标准确计算。
完成这一转换后,操作就变得非常简洁,险些只需要进行纹理采样的提取,这样可以确保纹理操作准确且高效地完成。


统统准确,低于70个周期

颠末前面的工作,纹理采样的计算已经基本完成,并且优化到每个循环镌汰到不到 70 个周期,这样的效率提升黑白常显著的。接下来,只管大部门主要工作已经完成,仍然有一些被剪切出来的代码片段没有完全处置惩罚,尤其是对于宽度的计算。现在,可以开始处置惩罚这些尚未完全初始化的部门,因为这些部门不再需要像之前那样以较为繁琐的方式进行初始化,可以使用更简洁的方式来完成后续的工作。
不再需要初始化Texel值

颠末优化后,代码已经不再报错,因为之前需要初始化的一些部门,现在通过整理后可以避免再次报错。接着,可以通过直接在行内进行处置惩罚,避免了不必要的初始化,进一步简化了代码。全部这些优化让每个像素的处置惩罚时间显着降低,现在每个像素的处置惩罚周期已经远低于 70 个周期,这显著提高了效率。对于混合和深度的处置惩罚也可以直接按这种方式完成。

统统都已SIMD化,除了纹理加载

在继续优化过程中,性能的提升大部门来自于将处置惩罚从 SIMB 转换到更高效的方式。目前,除了纹理加载(texel loads)外,险些全部的操作都已迁徙到 SIMD 中。由于暂时没有实用于 SIMD 的纹理提取(fetch),这一部门仍需要继续使用其他方法进行处置惩罚。不外,其他操作已经在 SIMD 中完成,接下来只需关注纹理提取及其上转换的部门。目的是将这部门操作也迁徙到 SIMD 中,从而进一步提高效率。
黑板:解包颜色数据

在处置惩罚像素写入时,目的是从目的数据中提取已打包的像素,这些像素是按某种次序打包的,如 BG、RA 等。需要将这些打包的像素解包成单独的分量,以便每个分量都能被提取并转化为向量形式。比方,对于蓝色分量(B),通过遮罩操作可以提取出蓝色分量的值,然后将其转换为浮动点值。同样的处置惩罚方式也实用于其他颜色分量。这种解包和转换的过程与之前打包过程的操作是逆向的,理论上可以正常工作。
使用掩码和移位提取颜色

要处置惩罚目的数据时,可以通过先进行遮罩操作来提取单独的颜色通道。比方,首先对目的数据进行遮罩操作,只保留蓝色通道的数据,然后将其转换为浮动点值。在处置惩罚过程中,使用了简朴的掩码和位移操作,确保准确提取和转换每个通道的值。在全部通道(如赤色、绿色、蓝色、透明度)上,采用相同的操作,只需要根据需要将其移动到准确的位置。此外,通过得当的位移操作,可以确保每个通道的数据位于准确的内存位置。
最后,这些操作以更加简洁、清晰的方式实现,避免了繁琐的过程和多次的提取步调,从而使代码更加简洁和高效。对于纹理的加载,也面对一些挑战,尤其是在没有直接加载纹理的情况下,需要进行一定的复制或模拟加载操作。



以下是您提到的不同大小的SIMD(单指令多数据)寄存器右移操作函数及其对应的指令的详细阐明:

  • _mm_srli_epi16:

    • 操作:将一个128位寄存器(__m128i)中的每个16位元素右移指定的位数(imm8)。
    • 指令:psrld(打包右移双字)。

  • _mm_srli_epi32:

    • 操作:将一个128位寄存器(__m128i)中的每个32位元素右移指定的位数(imm8)。
    • 指令:psrlq(打包右移四字)。

  • _mm_srli_epi64:

    • 操作:将一个128位寄存器(__m128i)中的每个64位元素右移指定的位数(imm8)。
    • 指令:psrlw(打包右移字)。

  • _mm_srli_pi16:

    • 操作:将一个64位寄存器(__m64)中的每个16位元素右移指定的位数(imm8)。
    • 指令:psrld(打包右移双字)。

  • _mm_srli_pi32:

    • 操作:将一个64位寄存器(__m64)中的每个32位元素右移指定的位数(imm8)。
    • 指令:psrldq(打包右移双字)。

  • _mm_srli_si128:

    • 操作:将一个128位寄存器(__m128i)按字节右移指定的位数(imm8)。
    • 指令:psrlq(打包右移四字)。

  • _mm_srli_si64:

    • 操作:将一个64位寄存器(__m64)中的每个64位元素右移指定的位数(imm8)。
    • 指令:psrldq(打包右移双字)。

这些操作通常用于在SIMD寄存器中对数据进行位级别的操作,广泛应用于高性能的多媒体处置惩罚、加密算法等领域。
黑板:样本读取矩阵

在进行纹理处置惩罚时,目的是将多个采样值(样本)归并成一个可以高效处置惩罚的格式。每个像素都有不同的采样值,比如样本A、样本B、样本C和样本D,每个样本都是对应某个像素的不同纹理样本。这些样本本身来自不同的位置,因此我们需要将它们整理成一个矩阵形式,便于后续的操作。
详细来说,处置惩罚时有四个像素(像素0、1、2、3),并且每个像素对应四个样本值(A、B、C、D)。为了高效处置惩罚这些像素,目的是将这些样本值打包成一个128位的SIMD寄存器,这样可以像处置惩罚单一的像素一样一次性处置惩罚全部像素的全部样本。
为了实现这一点,计划将每个样本值依次打包成一个寄存器,每个位置对应一个像素的不同样本值。通过这种方式,能够利用SIMD并行处置惩罚的优势,对全部像素的全部样本进行操作,从而提高计算效率。

将样本数据打包到4宽的寄存器中

目的是将样本值打包成紧凑的格式,以便对每个像素进行高效的处置惩罚。通过将每个像素的样本值打包到一个128位的SIMD寄存器中,能够实现每个像素全部样本的并行处置惩罚。这意味着原先的数组布局将被更换成每个像素的打包寄存器,每个寄存器包罗该像素的全部样本。
一旦样本值被打包成128位的寄存器,就可以像处置惩罚普通的SIMD值一样对它们进行操作。这种方式可以显著提高处置惩罚效率,因为全部操作都可以并行完成,而不需要单独处置惩罚每个像素的每个样本。
一些疯狂的emacs宏功夫

通过将样本值转换为打包格式,可以继续对其进行处置惩罚,雷同于原先的操作。现在,每个样本已经被打包到寄存器中,可以像原始的目的一样进行处置惩罚。通过这种方式,处置惩罚每个像素样本的效率得到显著提高,且由于并行操作,周期计数持续降低。这种优化使得处置惩罚变得更加高效,镌汰了不必要的计算,并且简化了操作流程。
对于vscode 的多行编辑功能

选中要编辑的内容一顿ctrl+d后修改


按照与目的相同的方式处置惩罚Texels

通过将每个纹理样本(如TexelDr,TexelDg,TexelDb,TexelDa等)与之前处置惩罚的目的样本进行雷同的打包处置惩罚,可以大大简化代码。每个样本只需要按照已经准备好的打包格式进行处置惩罚,就像之前解包目的一样,这样就能得到一个简洁且高效的代码段。通过删除全部冗余的标量版本代码,可以让每个纹理样本的处置惩罚变得更加简洁且高效。终极,全部纹理样本的处置惩罚都可以同一简化,镌汰了复杂的重复代码,并且优化了处置惩罚流程。

正常工作的纹理读取,险些是每像素50周期

颠末转换后,处置惩罚流程已经优化,成功将代码转换为SIMD格式后,每个像素的处置惩罚时间缩短到约莫50个周期,这还没有进行其他优化,因此处置惩罚效率已经显著提高。现在,大部门操作都已成功简化,只剩下纹理获取部门还未完全优化。固然可能还有遗漏的地方,但整体效果看起来很不错。
此外,可以实验进一步简化代码,看看是否可以不再设置某些值。计算了u和v向量后,生成了右侧掩码,并开始思考如何优化这个过程,进一步提高代码的效率。
如果掩码中没有内容怎么办?

如果写掩码(write mask)全为零,意味着没有像素需要写入,这时可以通过在整个处置惩罚流程中加入一个条件判断(if语句),避免实验不必要的工作。当确定全部像素都完全在添补地区之外时,就可以跳过这些像素的处置惩罚,节流计算资源。
为了实现这一点,可以使用 _mm_movemask_epi8 指令。该指令从每个8位元素的最高有用位生成一个掩码,并将效果存储到标量中。通过对右侧掩码实验 _mm_movemask_epi8 操作,如果某个像素需要添补,掩码中会相应地标志,只有在掩码有值时才实验后续工作。这种方式本可以进一步提高效率,但在实际测试中,并没有显著提升性能。
只管如此,仍然可以利用 _mm_movemask_epi8 进行标量测试,尤其是在处置惩罚大量像素时,可以在需要时跳过不必要的计算。

_mm_movemask_epi8 是一个 SSE(Streaming SIMD Extensions)指令,用于从一个 128 位的整数向量(__m128i 类型)中提取每个字节的最高有用位(MSB)。该指令会将这些最高有用位打包成一个 16 位的整数,返回一个效果,表现每个字节的最高有用位是 1 照旧 0。
使用场景

_mm_movemask_epi8 通常用于快速检查一组数据的条件。比方,它可以用来判断多个条件是否满足,或者检查哪些元素符合特定条件。这个操作是高效的,因为它能够在一次 SIMD 操作中处置惩罚多个字节。
语法

  1. int _mm_movemask_epi8(__m128i a);
复制代码


  • a 是一个 __m128i 类型的向量(包罗 16 个字节),该函数会提取每个字节的最高有用位,打包为一个 16 位的整数。
返回值

返回一个 int,该整数的每个二进制位对应于 __m128i 向量中每个字节的最高有用位。如果某个字节的最高有用位为 1,则对应的二进制位为 1,否则为 0。
示例

假设有一个 128 位的 __m128i 向量,包罗 16 个字节,调用 _mm_movemask_epi8 后,返回的整数可以通过其二进制表现来检察每个字节的 MSB:
  1. __m128i data = _mm_set_epi8(0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0);
  2. int mask = _mm_movemask_epi8(data);
  3. printf("Mask: %x\n", mask);  // 输出: Mask: 8000
复制代码
在这个例子中,data 向量的第 8 个字节的最高有用位为 1,因此输出的掩码为 0x8000。
常见应用



  • 条件判断:通过检查哪些字节的最高有用位为 1,能够在处置惩罚大量数据时快速判断哪些元素符合特定条件。
  • 早期退出:可以用于镌汰不必要的计算,比方,在处置惩罚像素、矩阵、向量等时,避免对不需要处置惩罚的数据做进一步的运算。
这种操作的优势在于可以同时处置惩罚多个字节,在 SIMD 计算中极大提高效率。
你不能一开始就将X坐标对齐到4像素边界,然后使用对齐加载和存储吗?

确实可以通过将像素边界对齐到预定位置来使用对齐加载和存储操作,这样可以提高内存访问效率。然而,暂时并不打算这样做,缘故原由在于当前仍然有一些题目需要解决。详细来说,宽度和高度的计算不完全准确,导致可能会覆盖数据。因此,必须先准确分配帧缓冲区,并确保内存对齐,以便全部操作能够正常工作。
在完成这些基本设置之后,才会考虑对齐操作。只管对齐可能不会对当前的处置惩罚器产生显著影响,但仍然计划进行测试以确保性能的提升。
你是不是很快就要把这段代码移到地面溅射中了?

代码将很快移植到地面平坦地区。实际上,如果启用了地面分割,它应该已经能在当前情况下正常工作。暂时关闭了地面分割,因为它消耗了太多时间。地面分割在被调用时会占用大量资源,固然它现在确着实处置惩罚地面平坦时被使用,但由于它的计算量较大,目前还没有显著加速,可能无法带来显着的性能提升。
目前的代码处置惩罚仍然非常低效,尤其是由于正在进行透视分割,导致添补的工作量巨大。为相识决这个题目,可以考虑增加地面缓冲区的大小,从而镌汰每帧都需要添补缓存的次数,这样可能会改善内存缓存的使用效率,减轻卡顿现象。
然而,在实现地面分割代码之前,仍然需要做很多优化和调整工作。只管目前没有启用地面分割,但计划在完成相干的优化后尽快启用。

是不是我感觉颠末这次SIMD转换后,每像素周期变得更一致了?

颠末这次SIMD转换后,每个像素的周期变得更加一致了。固然不完全确定这种变革是否可能,但目前看起来是这样的。
我错过了几天,不知道单独使用SSE2的CPU内建指令对你的游戏代码有不好影响吗,照旧我们正在面向SSE2?比方,是否应该将全部内容封装到平台特定的文件中,这样更轻易支持其他平台?

在游戏代码中,直接使用CPS进行优化(尤其是针对SSC平台)并不是一个好主意,因为在代码优化和抽象层设计之前,还需要确保基础功能的实现。通常,第一次编写例程时,会直接针对目的平台进行开辟,然后再考虑移植和优化。如果没有特别的需求或明确的目的,就不应该提前为跨平台做抽象。
在本例中,固然可以通过宏来更换内建操作以支持其他平台,但提进步行抽象并不会带来额外的好处。至于将代码移植到其他平台的可能性,可能性较小。除非是在其他平台上有特别的需求(比如物理计算等),但对于渲染部门,基本上不需要考虑将其移植到SSC2以外的平台,特别是对于像iPhone、Android或树莓派这类平台,软件渲染的需求并不大。SSC2更多是用来明确如何为软件渲染进行代码优化,并深入相识整个渲染流程,但实际操作中,渲染代码不太可能会被移植到其他平台。
对于没有指定吞吐量的内建指令,这是什么意思?

当提到没有明确指定吞吐量的内建函数(intrinsics)时,通常意味着该内建函数的实验速度或处置惩罚能力没有在文档中给出详细的数值。这种情况下,无法准确预期它的性能表现,可能需要通过实际的测试或基准测试来相识其在特定硬件上的表现。
不同类型的内建函数(如数学运算、向量操作等)可能在不同的处置惩罚器架构上有不同的吞吐量,甚至在相同架构上也可能受到其他因素(如内存访问模式、指令流水线等)的影响。因此,在没有明确吞吐量的情况下,开辟人员可能需要进行更多的性能优化和实验,以确保步伐能够高效运行。
如果不先加载目的,直接跳过而用掩码写入,比方 _mm_maskmoveu_si128,是否会更快?

有一个题目是,是否跳过加载目的,直接使用带有掩码的操作(如 _mm_maskmoveu_si128),这样做是否会更快。通常要回答这个题目,需要实际进行实验。
对于这种掩码移动操作,它会根据掩码条件有选择地将元素从一个数组存储到内存中,未满足掩码的元素不会被存储。而且,掩码移动操作在非对齐的情况下好像并没有惩罚,意味着即使目的地址没有对齐,它也能正常工作。
为了测试这种方法,首先需要看看掩码移动的实现方式和它的效果。通过实际测试,发现使用掩码移动后的性能实际上比不使用它慢了三倍,只管原本的预期是掩码移动会更快。
固然这个效果有些令人困惑,且目前无法表明为什么使用掩码移动会更慢,但在这个简朴的案例中,不使用掩码移动显然要更快,甚至是三倍的速度差异。
是否应该在我们全部的步伐中都使用SIMD来进行全部数学运算?

在全部步伐中对全部数学操作使用四倍宽度(SIMD)操作的题目是复杂的。固然在某些情况下,SIMD 确实能带来好处,尤其是在能够同时处置惩罚四个操作的场景下,但并不是全部情况都得当。
首先,如果步伐的计算不能并行实验四个操作,那么使用 SIMD 可能不会带来任何显著的提升。通常,只有在计算可以同时处置惩罚多个数据(如四个数据)时,才会从四倍宽度的操作中获得性能提升。但并不是全部代码都能设置为并行实验四个操作,很多时候数据处置惩罚无法达到这种程度。
此外,使用 SIMD 编译器内建指令可能会导致代码的复杂性增加,尤其是在需要对数据进行重新格式化、转换等操作时,这样的工作量可能得不偿失。而且,即使使用四倍宽度操作能够提升性能,实际的性能提升也可能有限。因此,最好照旧针对那些确定能够从中获益的代码进行优化,而不是盲目地将全部操作都转换为四倍宽度操作。
吞吐量为1的内建指令示例:_mm_cmpgt_ps

关于没有吞吐量(throughput)的指令,题目的讨论主要会合在 `_mm_cmpgt_ps 指令上。分析过程中,固然没有立即找到关于吞吐量的明确阐明,但通过检察指令的文档,发现该指令可能与浮点数比较操作相干,详细为“compare pack single precision floating point values”。
指令文档显示,吞吐量的设置为2,但这个信息并不显著,因此无法确认是否确实是吞吐量为2。为了更准确地明确,可能需要深入研究更多的技术文档或者与更熟悉该指令的专家进行交流。
在分析过程中,也注意到了一些关于指令实现的细节,比方一些指令可能通过软件模拟来处置惩罚大于或大于等于的比较操作,或者采用反向关系进行实现。因此,吞吐量的题目可能与这些实现方式的差异有关。
grumpygiant Agner Fog说吞吐量是1

有观点以为吞吐量是1,这个说法好像也合理。但也存在不确定性,因为在某些文献中提到的吞吐量值为“-”,这让人感到困惑。甚至有地方将“PS”列为指令,这也让人更加不解。若能得到Intel方面的表明,可能有助于更清晰地相识情况。终极,大家的讨论好像到了一个阶段,无法进一步推进。

应该是从前文档没更新
[什么是延迟与吞吐量?]

延迟是指从开始到竣事实验某个指令所需的总时间。而吞吐量则是指在没有中断的情况下,能够在一定时间内完成多少个指令实验,吞吐量通常因为指令之间的重叠而提高。
构造的终极目的通常是盼望将延迟降低到一个特定的阈值以下,从而提高整体效率。
优化的终极目的是什么,是要达到某个阈值,照旧仅仅完成转换?

目前的优化目的是将全部内容转换完成,固然还没有做到这一点,但预计很快就能实现。我们的计划是完成转换后,估算每个像素所需的周期数。得出这个数字后,接下来会实验尽可能接近理论值,看看能达到什么程度。大家的进展都还不错,讨论也到了一个阶段。
SIMD对变量的大小有影响吗,比如32位与64位的区别?

变量的大小确实很重要,缘故原由在于它决定了每次操作中能处置惩罚多少数据。比方,在不同的架构中,寄存器的大小不同:SCC寄存器是128位,AVX是256位,而AVX-512是512位。这意味着每个寄存器可以存储的数据量是由寄存器的位数与数据类型的大小决定的。
如果使用32位数据,每次操作中可以处置惩罚多个数据项:SCC可以处置惩罚4个,AVX可以处置惩罚8个,AVX-512可以处置惩罚16个。而如果使用64位数据,每次操作只能处置惩罚更少的项:SCC处置惩罚2个,AVX处置惩罚4个,AVX-512处置惩罚8个。
另外,某些操作在64位下可能比32位更耗时。比方,64位的除法操作可能比32位更慢。因此,选择合适的数据类型非常重要,通常建议使用尽可能小的数据类型,这样可以镌汰操作的成本并提高效率。
SSE代码有没有做任何缓存预取或提示相干的操作?

目前还没有进行缓存预取或提示等优化工作。实际上,直到现在我们还没有涉及到内存部门,所做的工作仅仅是对例程进行翻译。我们还没有进行广播、对齐等优化,也没有进行非暂时存储操作(只管在这种情况下,可能不需要使用非暂时存储)。
目前的工作主要会合在代码转换阶段,对于优化的影响还不确定。缓存预取可能会有所帮助,但需要通过测试才华确定效果。现在的实验速度相对较快,每个像素的时间约莫为50个周期,总的例程实验约莫需要200个周期。由于200个周期的循环没有太多时间可以用来进行内存等待,因此很难预见缓存预取能带来显著提升。但如果幸运的话,可能在等待期间有些操作可以进行缓存预取,从而节流部门时间。
我们能不能使用半精度浮点数而不是单精度浮点数,因为只有255个离散值不需要那么高的精度?

由于当前的硬件架构(比方SCC)不支持半精度浮点数(half float),所以不能直接使用半精度浮点数进行运算。SCC不支持半精度浮点数的乘法等操作,因此不能使用它们。固然可以实验本身实现半精度浮点数的支持,但这可能比直接使用16位定点数更加复杂且耗时。
如果要实现雷同功能,16位定点数可能是一个更合适的选择,因为它比32位浮点数更轻易实现,而且16位定点数可能在性能上更有优势,只管这仍然难以确定。由于当前的例程中数学运算的数量不多,可能通过奇妙的设计,利用16位定点数能够提升效率,从而能够同时处置惩罚更多像素。
如果能够将操作保持在相似的数量级,同时使用16位定点数,这将大大提高处置惩罚能力,每次可以处置惩罚更多像素,这是一个非常值得探索的方向。
法线贴图代码会转换为SIMD吗?

计划是终极将法线贴图代码转换为SIMD,但目前还需要明确如何详细实现。我们会在搞清楚其工作原理之后,着手进行转换。

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

傲渊山岳

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表