游戏引擎学习第116天

news/2025/2/22 12:31:15/

回顾昨天的工作

本次工作内容主要集中在游戏开发的低级编程优化,尤其是手动优化软件渲染。工作目的之一是鼓励开发者避免依赖外部库,而是深入理解代码并进行优化。当前阶段正进行SIMD(单指令多数据)优化,使用Intel推荐的编译器内建指令,这些指令在现代编译器(如MSVC、GCC、LLVM等)中广泛支持。尽管目前的代码实现是在x86架构下进行的,但这些优化方法对其他平台,如ARM架构(例如NEON)也同样适用,区别主要在于使用宏来切换不同平台的内建指令。

在实际的优化过程中,已经对绘制矩形的代码进行了SIMD优化,特别是对每四个像素的计算进行了并行化。此类优化不仅提高了处理速度,也展示了如何通过使用编译器提供的SIMD指令,使得代码在不同硬件平台之间能够保持兼容。

为了方便开发者跟进,已经提供了最新的源代码,确保每个人都能使用相同的版本来进行优化练习。接下来的计划是将更多代码转化为SIMD,以进一步提升性能,特别是在矩形绘制及其他数学运算方面。

切换到 -O2

今天的工作开始前,首先需要做一件简单的事情,以便更清晰地观察到当前代码的运行情况。首先,回顾一下之前实现的代码,它包含了一个日志调试循环计数器,用于展示代码运行时所消耗的周期数。然而,由于当前编译是在调试模式下进行的,这些周期数可能会显得非常不准确。

因此,计划将编译设置切换回优化版本,使用-O2优化选项。这将防止编译器做出一些不必要的调整和错误优化,从而更真实地反映代码的性能。切换后的结果显示,实际的周期数相比之前有了显著的增加。这是因为引入了SIMD(单指令多数据)优化后,代码结构变得更复杂。虽然原理上代码执行的基本内容没有变化,但由于引入了额外的操作,导致编译器在处理时变得更加混乱,影响了整体性能。

这种性能下降是预期中的,因为目前的代码已经不再是最优的状态,且编译器需要非常智能地去优化这些额外的计算操作,例如尝试将多个操作融合成更高效的指令。尽管如此,这种性能损失并不影响整体优化过程的进展,毕竟之前的性能也并不理想。因此,这一阶段的目标是通过优化编译选项,逐步提升代码的性能。
-O2 貌似我的只能release 生效

考虑在更广泛的范围内进行 TestPixel TIMED_BLOCK 测试

考虑到性能问题,目标是优化代码的计时方法,以避免每次处理四个像素时都进行时间戳计数器的调用。这些计时操作会分别在每个“开始时间块”和“结束时间块”时调用指令来获取时间戳计数器的值,导致增加了额外的指令开销。每次调用这些指令都会访问存储在缓存中的时间戳计数器,而这些操作可能会增加工作量,甚至可能污染缓存。

如果我们在每处理一组四个像素时都执行这些时间戳相关的操作,实际上是在增加计算量,并且可能影响到缓存的效率,从而降低程序整体性能。因此,需要改进这一计时方式,避免在每个像素块处理时都增加额外的时间获取操作,而是寻求更高效的方式来衡量每个像素的周期消耗。

game_render_group.cpp: 将计时器移动到 for 循环周围

为了优化计时方法,计划将计时器移到外部,只在实际执行填充操作的代码块周围添加计时器。通过将计时器集中在填充操作或仅仅是for循环的部分,可以避免每次处理像素时都进行时间戳计数器的更新。这意味着计时将更加高效,只记录实际的计算过程,而不是每个像素或每次循环迭代时都执行额外的计时操作。

这样做后,编译并运行代码后,能够查看性能是否得到了改善。
在这里插入图片描述

调试器: 看到有两个循环几乎相同

在代码中,存在两个几乎相同的循环,分别执行不同的测试任务。为了更好地识别它们的功能,决定为这些循环添加标识,使得更容易查看并区分每个循环的作用。此时,循环尚未命名,因此需要进行一些调整来方便区分。
在这里插入图片描述

在这里插入图片描述

game_platform.h: 为这些 DebugCycleCounters 编号

目前代码中的循环没有名称,导致区分它们有些困难。虽然可以通过查看数字来识别,但为了更好地理解和调试,最终会对这些部分进行改进,确保它们更易于区分。现在的重点是逐步进行改进,每次专注于解决一个问题。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_render_group.cpp: 将 TestPixel 重命名为 ProcessPixel,并移除 DrawRectangleSlowly 的 TIMED_BLOCK

现在将 TestPixel 重命名为 ProcessPixel,更准确地反映了当前的功能,即处理所有像素。之前的名字已经不再适用,新的名字更加符合代码的实际作用。接着,删除了不再需要的部分,清理了冗余代码。

调试器: 查看 DEBUG CYCLE COUNTS

现在,代码中只剩下了 ProcessPixelDrawRectangleHopefullyQuickly 两个调用,每个都被调用了202次。可以看到,每次调用的总时间有所不同,主要的差异来自于设置成本。大部分的时间都花费在了循环中,这些循环占据了大部分的执行时间。

game_render_group.cpp: 引入 END_TIMED_BLOCK_COUNTED

现在的目标是计算每个循环所处理的像素数。通过已知的 XMaxYMax,可以计算出矩形的面积,即填充的像素数量。接下来,通过计算填充的像素数量,可以在结束时间块时将这个像素数作为参数传入。

具体实现方式是:使用之前的 END_TIMED_BLOCK 宏,并传入计算得到的像素数量。然后,不再单独对命中次数进行累加,而是直接将填充的像素数加到命中计数器中。

这样就可以清楚地统计每个循环所花费的时间和填充的像素数量,进一步优化性能。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

调试器: 看到 ProcessPixel 计数现在更准确 [70cy/h][count/hit]

现在,当程序运行时,可以看到填充像素的计数变得更加准确了。之前看到的奇怪的900值,问题并不是因为编译器过于混乱,而是因为在某些地方错误地放置了结束计时块(END_TIMED_BLOCK)。通过调整后,现在我们已经在整个循环外部计时,然后通过处理的像素数来进行除法计算,这样就可以得到每个像素的周期数。

这种方法避免了为每个像素块单独计时,从而减少了因时间统计引入的不准确性。通过这种改进,现在可以得到更精确的每像素周期数,而不需要在每个像素块中插入计时指令,这有效减少了误差,确保了性能数据的更高准确性。

game_render_group.cpp: 用 SIMD 编写代码

接下来,将演示如何实现某些代码的并行化。在处理时,首先定义了一个四值寄存器,并将原来的 texel 替换为这个寄存器。通过将 texel 作为寄存器传递,能够在计算中并行处理四个值。为此,代码先加载 texel 并设置其中的一个成员值。

在实现中,为了兼容不同编译器,使用了 Visual Studio 编译器的特殊语法来访问寄存器的成员。尽管在某些编译器(如 GCC)中可能无法使用此语法,但为了简化代码,将采用一种更通用的做法,即将 texel 转换为 float 指针来进行访问和赋值。

接着,解释了如何通过指针直接操作内存中的四个值,而不是每次通过四个循环进行单独计算。这种方式优化了代码的效率,使得多个值能够并行计算。通过将四个浮点值与常量值进行逐一相乘,可以使用处理器的 SIMD(单指令多数据)指令集提高效率。

在代码实现中,使用了 ps(packed single precision)指令,它会同时对四个浮点值进行乘法运算,从而加速计算过程。此外,对于常量的处理,使用了广播操作,即将常量的值应用到四个值的每个位置,从而实现向量化的运算。

最终,代码通过这种方式简化了操作,使得每个操作只需要进行一次,而不是在循环中处理每个单独的值,提升了计算效率。这种做法在优化过程中非常有用,尤其是在需要处理大量数据时。
在这里插入图片描述

运行并看到仍然产生正确的结果

当运行这段代码时,观察到它仍然能够生成正确的结果,至少从目前来看是这样。因此,整个过程应该是顺利的,代码已经能够按照预期执行。
在这里插入图片描述

切换到 -Od 这个应该是debug模式吧

暂时关闭定时功能,因为此时并不需要进行计时,只是在处理代码。可能会打开调试模式,这样就能看到代码中发生了什么。目前还不清楚为什么渲染文件无法保持打开状态,希望能够快速解决问题,尤其是在处理矩形和相关位置的计算时。

调试器: 检查 TexelAr

接下来,演示了调试过程,主要目的是查看变量的变化情况。在调试时,程序会进入填充过程,但由于一些原因,变量的值未能正确加载,导致结果为空。为了更清楚地观察到实际的值,通过设置断点,观察变量 texel 的值。经过调试,发现加载的值是正确的,并且进行了乘法运算,将其规范化为浮动值范围。接着进行了线性近似的平方操作,这些计算都同时完成,避免了在循环中逐个操作四个值。整个过程的重点是,使用并行处理方式替代循环,通过简单的方式完成了任务,从而提高了效率。
在这里插入图片描述

game_render_group.cpp: 继续将这些 Texel 计算转换为 SIMD

本段代码和讨论主要集中在使用宏和内联函数优化代码。首先,展示了如何使用相同的操作方式处理不同的变量,比如 a ba g,并且通过宏(例如 mn_square)来简化代码结构,使得操作更加直观和易于维护。这种方式允许将多个重复的操作合并为一个简洁的宏,从而避免了冗长的代码。

接下来,讨论了如何将纹理颜色从 0 到 255 的范围转换为 0 到 1 的线性空间值,操作时会对纹理的每个分量执行相同的操作,并且强调了这种方式的高效性。通过处理这些操作,可以避免手动循环处理每个颜色分量,从而加速代码执行。

接着提到,代码中还可以进一步简化,甚至通过将操作“宽化”(wide operation),即一次性处理多个值来进一步提高性能。例如,通过将整个操作扩展为对一组数据进行并行处理,而不是逐个处理。

在处理宏和操作时,代码的可读性和清晰度得到了提升,尤其是对于初学者,保持操作的透明性和清晰的步骤有助于理解。此外,虽然可以通过宏进一步简化代码,但为了避免过度抽象,当前保持代码的一部分展开,以便于理解每一步的操作。

最后,提到了一些实现上的细节,例如如何在代码中处理像素值的设置和获取,尤其是在涉及到掩码(masking)操作时,这部分的处理会在之后进行优化。

整体而言,优化的核心目标是通过宏和高效的操作方式简化代码结构,提高执行效率,并确保代码在处理纹理转换时不会丢失清晰度。同时,代码的改进是在不断迭代中进行的,重点是逐步转换为更高效的形式,并解决后续可能出现的编译器问题。
在这里插入图片描述

运行并注意到我们运行得很顺利 [65cy/h]

目前代码运行良好,尽管编译方式已经调整为完全并行处理所有纹理操作。所有之前在循环内部进行的操作,现在已经被移到循环外部进行统一处理。这种优化的结果是,原本在每次循环中重复执行的操作,现在只需要一次性处理,即一次性对四个像素进行操作。

通过将这些操作集中处理,代码的效率得到了显著提高,减少了不必要的重复计算。最终,所有纹理数据的处理都在一个批处理中完成,从而提升了程序的整体性能。

game_render_group.cpp: 继续将这些操作

接下来,需要继续优化代码结构,不仅是将操作移出循环,还要进一步扩展操作的范围,使其更加广泛和高效。具体来说,需要将相关的变量处理改为更广泛的版本,以便在处理多个像素时能更高效地执行。

首先,针对一些已定义的变量(如Color),需要将它们处理为“Colorr_4x,Colorg_4x,Colorb_4x,Colora_4x”,即通过扩展这些值,使得它们可以同时应用于多个像素。这样,所有的颜色值都能在一次操作中同时处理,而不是逐个像素进行处理,这将大大提高处理效率。

为了确保不会发生命名冲突,临时使用了__m128 fx __m128 fy前缀,待最终完成后会去掉。在处理颜色值时,需要将它们转换为相应的“宽版本”,以便能够进行多重像素操作。常见的处理方式包括对每个像素的颜色值进行乘法操作,将其乘以常数。这里需要注意的一点是,当乘法常数传入时,要确保常数的版本也是“宽版本”的,这样每个像素的操作能够同步进行。

此外,已经完成的乘法操作将转换为宏操作,确保每次使用的常数是适配整个像素集的。在加法操作上,也会使用类似的方式,将三个加法操作分解开来,确保每次都能够对所有像素进行加法处理。

对于一些简单的操作(如减法),同样采用了类似的处理方式。整个过程看似简单,但通过将常数和变量进行宽版本处理,可以大大提升并行计算的效率,最终实现了更加高效的像素处理。

最后,所有这些处理会被重复应用到颜色值的每个通道(如RGB通道),确保每个通道的计算都是并行进行的,最终完成了更高效的图像混合和计算。
在这里插入图片描述

编译并检查是否有错误 [57cy/h] (release)

现在,已经将这段代码的更多部分转换成了更大的处理块。所有相关操作现在都在一个较大的范围内完成。接下来,通过检查代码来确认是否有任何错误。从目前的观察来看,代码看起来没有问题,依旧保持较为清晰简洁。经过这些改动,程序应该运行得更加顺畅且高效。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_render_group.cpp: 将剩余的操作变宽,除了 Clamp

现在,需要继续进行一些操作。首先,通过忽略clamp部分,其他部分应该能够顺利完成。因此,将所有的操作代码写出,除了clamp。这些操作应该很容易识别并且可以进行宽操作处理,所有这些内容都可以用我们之前做过的操作进行处理。例如,之前做的mmSquare就是简单的乘法操作,已经处理过的“One255”常数版本也可以直接进行宽化。

这些操作看起来没有什么复杂之处,实际上就是常见的操作转换。接下来,将所有这些操作做宽化处理,所有乘法操作和加法操作都可以扩展到宽版本。对于处理常数,也可以轻松完成,唯一剩下的复杂操作是平方根,但目前仍然保留在代码中,等待后续处理。

Intel Intrinsics Guide: _mm_sqrt_ps1

对于平方根操作,虽然它有些慢,但幸运的是,已经有现成的解决方案。在英特尔的内在函数指南中,可以找到相关的平方根实现。通过搜索平方根,能看到所有可用的选项。为了简化处理,选择最简单的选项,即mm_sqrt,这将用于当前的平方根计算。
在这里插入图片描述

在这里插入图片描述

game_render_group.cpp: 实现 _mm_sqrt_ps 并继续转换为 SIMD

目前,平方根操作采用了 mm_sqrt,虽然这不是最快的解决方案,但可以满足需求。如果需要更高效的处理方式,可以进一步优化。接下来,需要将一些变量转换为适应新的方式,因为这些变量现在也需要被处理为宽版本(wide)。完成了所有相关操作后,运行程序时应该除了 clamp 以外的一切都能正常工作。

在检查过程中,发现忘记处理一个常量,并且需要通过 m 来访问该常量。修正后,程序应能正常运行。
在这里插入图片描述

运行并注意到我们正在正确地进行图像处理 [52cy/h] release

目前,已经基本完成了所有操作,只剩下 clamp 需要处理。接下来,需要着手解决 clamp 的问题,使其能够正常工作。
在这里插入图片描述

在这里插入图片描述

位图红色修改的有问题
在这里插入图片描述

没改对
在这里插入图片描述

在这里插入图片描述

调试器: 查看 Clamp01 的作用

当前实现的 clamp 操作生成了不必要的跳转指令,这些跳转是在检查值是否超出范围时产生的。这样做导致了效率低下,因为每次判断是否需要替换值时都会生成跳转,而这种方式并不理想。目标是避免这些不必要的跳转,采用更直接的方式来处理 clamp,以提高效率。
在这里插入图片描述

Intel Intrinsics Guide: _mm_min_ps 和 _mm_max_ps

可以利用 SIMD 中内置的函数来简化 clamp 操作,这些函数能够在一个操作中同时执行比较和替换,从而提高效率。通过使用最小值和最大值的比较操作,可以避免不必要的跳转。具体而言,最小值操作会保留较小的值,而最大值操作会保留较大的值。通过这两个简单的指令,可以实现有效的 clamp 操作,而无需多余的判断和跳转,从而提高性能。
_mm_min_ps_mm_max_ps 是用于 SIMD(单指令多数据)操作的指令,它们是 Intel 的 SSE(Streaming SIMD Extensions)指令集的一部分,通常用于处理浮点数(float 类型)的最小值和最大值。它们分别执行两个向量中每个元素的最小值和最大值操作。

  • _mm_min_ps:这个指令将两个 __m128 类型的浮点数向量作为输入,并将这两个向量中每一对对应的元素进行比较,返回一个新的 __m128 向量,其中每个元素是输入向量中对应位置的最小值。

    例如,给定两个向量:

    __m128 a = _mm_set_ps(4.0f, 3.0f, 2.0f, 1.0f);
    __m128 b = _mm_set_ps(1.5f, 3.5f, 2.5f, 0.5f);
    __m128 result = _mm_min_ps(a, b);
    

    result 将包含 (1.5f, 3.0f, 2.0f, 0.5f),即每个元素是 ab 中对应位置的最小值。

  • _mm_max_ps:与 _mm_min_ps 相反,这个指令将两个浮点数向量中的每一对元素进行比较,并返回一个新的 __m128 向量,其中每个元素是对应位置的最大值。

    例如:

    __m128 a = _mm_set_ps(4.0f, 3.0f, 2.0f, 1.0f);
    __m128 b = _mm_set_ps(1.5f, 3.5f, 2.5f, 0.5f);
    __m128 result = _mm_max_ps(a, b);
    

    result 将包含 (4.0f, 3.5f, 2.5f, 1.0f),即每个元素是 ab 中对应位置的最大值。

这两个指令可以高效地并行处理浮点数数据,并且避免了使用传统的条件判断和跳转结构,从而提高代码的执行速度。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

game_render_group.cpp: 将 Clamps 操作进行宽化处理 [52cy/h]

为了实现对值的夹紧操作,可以通过使用 _mm_max_ps_mm_min_ps 来高效地完成,而无需显式的比较或跳转。具体步骤如下:

  1. 首先,Clamp到 0: 使用 _mm_max_ps 来确保值不小于零。这样,如果值小于零,就保留零;如果值大于零,就保留原值。

  2. 然后,Clamp紧到 1: 使用 _mm_min_ps 来确保值不大于 1。这样,如果值大于 1,就将其夹紧到 1。

通过这两条指令(_mm_max_ps_mm_min_ps),可以高效地完成值在 0 和 1 之间的夹紧操作。这里的优势在于,不需要使用循环或条件判断来显式进行比较。由于这些操作是 SIMD 指令集的一部分,它们能够并行处理数据,并且避免了分支,从而提高了执行效率。

这种方法非常简洁且高效,通常只需要两个周期就能完成夹紧操作,相较于传统的比较和条件跳转,性能得到了显著提升。

运行并注意到游戏已经更快了

通过简化后端的操作,移除了之前的复杂部分,代码已经变得更高效了。即使没有对加载部分进行优化,编译后的版本已经显示出性能提升。当前的周期计数已经下降,优化后的构建版本中,周期数已减少到接近 52 次,每个像素的处理时间大约是 52 次周期。虽然这个数值仍然不是理想的,但已经明显变得更低,性能逐渐得到了改善。

反思这项工作的简便性

最后,可能需要考虑的一点是,是否还可以进一步优化,虽然时间可能有限。可以考虑将一些操作改为宽度处理,进一步提升性能。整体来说,优化的过程其实并不复杂。通过使用内建的指令集来执行乘法、减法、加法等运算,这些操作会被转化为简单的单一指令,且非常直观。通过这种方式,编程几乎就是在模拟处理器将要执行的操作,虽然没有深入到寄存器级别的模拟,但大部分操作都通过内建指令进行了高效处理,整个过程是非常简洁明了的。

考虑剩下的 SIMD 转换部分

剩下的难点主要集中在处理填充和加载部分。填充操作会在像素经过混合处理后,将最终结果写回,这部分相对简单。而加载操作将会是非常复杂的部分,因为它涉及到纹理查找,这一过程本身并不适合简单化,尤其是这一部分是特别难以优化的。为了准备后续的工作,可以先对一些操作进行简化,比如处理像素数据等,为接下来的复杂部分做准备。

game_render_group.cpp: 进行 PixelP 的宽化处理

接下来,考虑将某些操作转换为标量计算,以便查看其效果。首先,提取像素的x和y值,并将其转换为标量形式。然后,通过计算dx和dy的内积,内积就是将每个分量相乘后加总。在标量形式中,执行相同的计算,即dx乘以x,dy乘以y,然后得到最终结果。这些步骤完成了标量计算的转换。
在这里插入图片描述

调试器: 调查编译器在那 50 个周期上的处理情况

在分析汇编代码时,发现编译器的优化表现得非常不同。最初的代码经过优化后,周期数明显减少,从每像素170个周期降到115个周期。这一变化令人困惑,因为看似没有做出重大改动。通过对比慢速版本和快速版本的汇编代码,发现编译器做出了意想不到的优化。特别是,原本的一些计算和函数调用被“折叠”到了外部代码中,导致了一部分计算不再执行,优化了整体的性能。

这说明了一些编程中的常见误解,特别是在谈论“零成本抽象”时。例如,使用内联函数(如简单的乘法和加法)或类型转换可能会让编译器难以优化,从而导致额外的性能开销。这一实例证明了在实际情况下,某些“零成本”抽象可能会带来显著的性能损失。
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

两个版本和对应的反汇编

快没有封装v2进行函数调用innerfor (int I = 0; I < 4; I++) {
#if 1int X = XI + I;
#if 1// 创建当前像素位置的 v2 坐标real32 PixelPx = (real32)(XI + I);real32 PixelPy = (real32)Y;real32 dx = PixelPx - Origin.x;real32 dy = PixelPy - Origin.y;// 计算U和V坐标,它们代表了在纹理中的位置,基于d和X轴、Y轴的内积real32 U = dx * nXAxis.x + dy * nXAxis.y;// U坐标,基于d和X轴的内积,乘以反向X轴的长度平方real32 V = dx * nYAxis.x + dy * nYAxis.y;
// V坐标,基于d和Y轴的内积,乘以反向Y轴的长度平方
#else// 创建当前像素位置的 v2 坐标v2 PixelP = v2i(X, Y);v2 d = PixelP - Origin;// 计算U和V坐标,它们代表了在纹理中的位置,基于d和X轴、Y轴的内积real32 U = Inner(d, nXAxis);// U坐标,基于d和X轴的内积,乘以反向X轴的长度平方real32 V = Inner(d, nYAxis);// V坐标,基于d和Y轴的内积,乘以反向Y轴的长度平方
#endif反汇编bool ShouldFill[4];for (int I = 0; I < 4; I++) {
00007FF9CA678AEE  mov         dword ptr [rbp+0AE4h],0  
00007FF9CA678AF8  jmp         __$EncStackInitStart+970h (07FF9CA678B08h)  
00007FF9CA678AFA  mov         eax,dword ptr [rbp+0AE4h]  
00007FF9CA678B00  inc         eax  
00007FF9CA678B02  mov         dword ptr [rbp+0AE4h],eax  
00007FF9CA678B08  cmp         dword ptr [rbp+0AE4h],4  
00007FF9CA678B0F  jge         __$EncStackInitStart+0FC5h (07FF9CA67915Dh)  
#if 1int X = XI + I;
00007FF9CA678B15  mov         eax,dword ptr [rbp+0AE4h]  
00007FF9CA678B1B  mov         ecx,dword ptr [rbp+5C4h]  
00007FF9CA678B21  add         ecx,eax  
00007FF9CA678B23  mov         eax,ecx  
00007FF9CA678B25  mov         dword ptr [rbp+0B04h],eax  
#if 1// 创建当前像素位置的 v2 坐标real32 PixelPx = (real32)(XI + I);
00007FF9CA678B2B  mov         eax,dword ptr [rbp+0AE4h]  
00007FF9CA678B31  mov         ecx,dword ptr [rbp+5C4h]  
00007FF9CA678B37  add         ecx,eax  
00007FF9CA678B39  mov         eax,ecx  
00007FF9CA678B3B  cvtsi2ss    xmm0,eax  
00007FF9CA678B3F  movss       dword ptr [rbp+0B24h],xmm0  real32 PixelPy = (real32)Y;
00007FF9CA678B47  cvtsi2ss    xmm0,dword ptr [rbp+584h]  
00007FF9CA678B4F  movss       dword ptr [rbp+0B44h],xmm0  real32 dx = PixelPx - Origin.x;
00007FF9CA678B57  movss       xmm0,dword ptr [rbp+0B24h]  
00007FF9CA678B5F  subss       xmm0,dword ptr [Origin]  
00007FF9CA678B67  movss       dword ptr [rbp+0B64h],xmm0  real32 dy = PixelPy - Origin.y;
00007FF9CA678B6F  movss       xmm0,dword ptr [rbp+0B44h]  
00007FF9CA678B77  subss       xmm0,dword ptr [rbp+314Ch]  
00007FF9CA678B7F  movss       dword ptr [rbp+0B84h],xmm0  // 计算U和V坐标,它们代表了在纹理中的位置,基于d和X轴、Y轴的内积real32 U = dx * nXAxis.x + dy * nXAxis.y;
00007FF9CA678B87  movss       xmm0,dword ptr [rbp+0B64h]  
00007FF9CA678B8F  mulss       xmm0,dword ptr [nXAxis]  
00007FF9CA678B97  movss       xmm1,dword ptr [rbp+0B84h]  
00007FF9CA678B9F  mulss       xmm1,dword ptr [rbp+34Ch]  
00007FF9CA678BA7  addss       xmm0,xmm1  
00007FF9CA678BAB  movss       dword ptr [rbp+0BA4h],xmm0  // U坐标,基于d和X轴的内积,乘以反向X轴的长度平方real32 V = dx * nYAxis.x + dy * nYAxis.y;
00007FF9CA678BB3  movss       xmm0,dword ptr [rbp+0B64h]  
00007FF9CA678BBB  mulss       xmm0,dword ptr [nYAxis]  
00007FF9CA678BC3  movss       xmm1,dword ptr [rbp+0B84h]  
00007FF9CA678BCB  mulss       xmm1,dword ptr [rbp+36Ch]  
00007FF9CA678BD3  addss       xmm0,xmm1  
00007FF9CA678BD7  movss       dword ptr [rbp+0BC4h],xmm0  
// V坐标,基于d和Y轴的内积,乘以反向Y轴的长度平方
#else// 创建当前像素位置的 v2 坐标v2 PixelP = v2i(X, Y);v2 d = PixelP - Origin;// 计算U和V坐标,它们代表了在纹理中的位置,基于d和X轴、Y轴的内积real32 U = Inner(d, nXAxis);// U坐标,基于d和X轴的内积,乘以反向X轴的长度平方real32 V = Inner(d, nYAxis);// V坐标,基于d和Y轴的内积,乘以反向Y轴的长度平方
#endif慢有封装v2进行函数调用inner
inline real32 Inner(v2 A, v2 B) {// 计算A的X分量与B的X分量的乘积// 然后加上A的Y分量与B的Y分量的乘积real32 Result = A.x * B.x + A.y * B.y;// 返回内积的结果return Result;
}for (int I = 0; I < 4; I++) {
#if 1int X = XI + I;
#if 0// 创建当前像素位置的 v2 坐标real32 PixelPx = (real32)(XI + I);real32 PixelPy = (real32)Y;real32 dx = PixelPx - Origin.x;real32 dy = PixelPy - Origin.y;// 计算U和V坐标,它们代表了在纹理中的位置,基于d和X轴、Y轴的内积real32 U = dx * nXAxis.x + dy * nXAxis.y;// U坐标,基于d和X轴的内积,乘以反向X轴的长度平方real32 V = dx * nYAxis.x + dy * nYAxis.y;
// V坐标,基于d和Y轴的内积,乘以反向Y轴的长度平方
#else// 创建当前像素位置的 v2 坐标v2 PixelP = v2i(X, Y);v2 d = PixelP - Origin;// 计算U和V坐标,它们代表了在纹理中的位置,基于d和X轴、Y轴的内积real32 U = Inner(d, nXAxis);// U坐标,基于d和X轴的内积,乘以反向X轴的长度平方real32 V = Inner(d, nYAxis);// V坐标,基于d和Y轴的内积,乘以反向Y轴的长度平方
#endif
反汇编bool ShouldFill[4];for (int I = 0; I < 4; I++) {
00007FF9CA6A8AEE  mov         dword ptr [rbp+0AE4h],0  
00007FF9CA6A8AF8  jmp         __$EncStackInitStart+970h (07FF9CA6A8B08h)  
00007FF9CA6A8AFA  mov         eax,dword ptr [rbp+0AE4h]  
00007FF9CA6A8B00  inc         eax  
00007FF9CA6A8B02  mov         dword ptr [rbp+0AE4h],eax  
00007FF9CA6A8B08  cmp         dword ptr [rbp+0AE4h],4  
00007FF9CA6A8B0F  jge         __$EncStackInitStart+0F79h (07FF9CA6A9111h)  
#if 1int X = XI + I;
00007FF9CA6A8B15  mov         eax,dword ptr [rbp+0AE4h]  
00007FF9CA6A8B1B  mov         ecx,dword ptr [rbp+5C4h]  
00007FF9CA6A8B21  add         ecx,eax  
00007FF9CA6A8B23  mov         eax,ecx  
00007FF9CA6A8B25  mov         dword ptr [rbp+0B04h],eax  
#if 0// 创建当前像素位置的 v2 坐标real32 PixelPx = (real32)(XI + I);real32 PixelPy = (real32)Y;real32 dx = PixelPx - Origin.x;real32 dy = PixelPy - Origin.y;// 计算U和V坐标,它们代表了在纹理中的位置,基于d和X轴、Y轴的内积real32 U = dx * nXAxis.x + dy * nXAxis.y;// U坐标,基于d和X轴的内积,乘以反向X轴的长度平方real32 V = dx * nYAxis.x + dy * nYAxis.y;
// V坐标,基于d和Y轴的内积,乘以反向Y轴的长度平方
#else// 创建当前像素位置的 v2 坐标v2 PixelP = v2i(X, Y);
00007FF9CA6A8B2B  mov         edx,dword ptr [rbp+584h]  
00007FF9CA6A8B31  mov         ecx,dword ptr [rbp+0B04h]  
00007FF9CA6A8B37  call        v2i (07FF9CA6A1622h)  
00007FF9CA6A8B3C  mov         qword ptr [rbp+0B28h],rax  v2 d = PixelP - Origin;
00007FF9CA6A8B43  mov         rdx,qword ptr [Origin]  
00007FF9CA6A8B4A  mov         rcx,qword ptr [rbp+0B28h]  
00007FF9CA6A8B51  call        operator- (07FF9CA6A11C7h)  
00007FF9CA6A8B56  mov         qword ptr [rbp+0B48h],rax  // 计算U和V坐标,它们代表了在纹理中的位置,基于d和X轴、Y轴的内积real32 U = Inner(d, nXAxis);
00007FF9CA6A8B5D  mov         rdx,qword ptr [nXAxis]  
00007FF9CA6A8B64  mov         rcx,qword ptr [rbp+0B48h]  
00007FF9CA6A8B6B  call        Inner (07FF9CA6A123Ah)  
00007FF9CA6A8B70  movss       dword ptr [rbp+0B64h],xmm0  // U坐标,基于d和X轴的内积,乘以反向X轴的长度平方real32 V = Inner(d, nYAxis);
00007FF9CA6A8B78  mov         rdx,qword ptr [nYAxis]  
00007FF9CA6A8B7F  mov         rcx,qword ptr [rbp+0B48h]  
00007FF9CA6A8B86  call        Inner (07FF9CA6A123Ah)  
00007FF9CA6A8B8B  movss       dword ptr [rbp+0B84h],xmm0  // V坐标,基于d和Y轴的内积,乘以反向Y轴的长度平方
#endif

在这段代码的两个版本中,可能存在显著的性能差异,主要是由于不同的实现方式以及数据处理和函数调用的方式。具体来说,差异可以归结为以下几点:

1. real32 数值运算 vs v2 向量运算

  • 版本1 直接使用了 real32 类型的标量值来计算每个像素位置的 UV 坐标。这个方式通过简单的标量运算,如内积、加法和乘法,通常比向量操作更高效,因为它减少了结构体访问和更多的运算步骤。
  • 版本2 使用了 v2 类型来表示像素坐标,并通过 Inner 函数进行内积计算。每次操作都需要通过函数调用,这增加了开销。对于每个像素位置,v2 结构体中的 xy 坐标需要单独访问并进行计算,而函数调用会带来额外的栈操作和上下文切换。

2. 内存访问模式

  • 版本1 中,real32 变量被连续地存储和操作,这通常更适合 CPU 缓存,因为它涉及对基本类型的连续内存访问。
  • 版本2 中,v2 结构体可能导致更多的内存访问(如访问 v2xy 分量),尤其是在使用结构体时,编译器可能无法完全优化对内存的访问模式。此外,结构体的成员可能需要更复杂的指针操作,从而影响性能。

3. 函数调用的开销

  • 版本1 直接在代码中进行计算,不需要额外的函数调用。
  • 版本2 在计算 UV 时使用了 Inner 函数,每次计算都涉及到函数调用。这不仅增加了栈帧的开销,还可能降低 CPU 缓存的命中率,特别是如果 Inner 函数没有被内联化。

4. 向量化优化

  • 版本1 更有可能被现代编译器优化为 SIMD(单指令多数据)指令,这种指令可以在一个周期内处理多个数据点,因此能有效加速计算。
  • 版本2 中的 v2 结构体可能无法被 SIMD 优化,尤其是在内积函数中进行逐个分量的计算时,可能缺少一些并行计算的优势。

5. 临时变量和中间计算

  • 版本1 中,UV 的计算依赖于标量 dxdy,这些变量在内存中的使用相对直接。
  • 版本2 中,计算涉及更复杂的数据结构和中间步骤。每个 v2 变量都需要在内存中进行更频繁的复制和操作,增加了计算时间。

总结

性能差异的根本原因是 版本1 采用了简单的标量计算,避免了额外的结构体操作和函数调用,能够更好地利用 CPU 的缓存和并行处理能力。而 版本2 引入了更复杂的结构体运算和函数调用,导致更多的内存访问和处理开销,进而导致较大的性能差异。

如果你的目标是优化性能,建议使用 版本1 中的标量运算方式,尽可能减少不必要的函数调用和数据结构访问,或者考虑将 Inner 函数内联化,并利用向量化指令来加速计算。

game_render_group.cpp: 完成 SIMD 转换

主要目标是优化循环中的计算,尤其是将一些变量移出循环,以提高性能。具体的步骤包括:
在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

运行并注意到进展缓慢

在这段讨论中,主要内容集中在优化循环以及一些计算的转换上。尽管已经进行了一些优化,代码依然在逐步推进中。以下是几个要点:

  1. 优化进程的推进:虽然已经将部分计算移出了循环,但优化仍在继续进行中,目标是进一步提高效率。尽管有了些进展,但依然在逐步推进的过程中。

  2. 对优化的喜好:对于执行这种优化转换的过程,有一定的兴趣。虽然这些优化并不是常做的任务,但在进行时却发现这种工作有趣,可能是因为平时并不常涉及此类优化,或者因为这类工作提供了一些新鲜感。

  3. 偶然的工作:在这周,也恰好涉及了某些类似的优化任务,尽管这些任务是偶然发生的,并不代表常规工作的一部分。

整体而言,尽管优化工作在进行中,且通过移出循环等措施逐步提升效率,但并不常做这类优化工作,因此对这种任务的处理保持了一定的兴趣。同时,也提到这次优化的任务和其他偶然的任务并无太大关联,更多的是一次性地涉及到的内容。

回顾并展望未来将如何在 SIMD 中进行加载和重新打包

在这段内容中,重点讨论了代码优化的过程,以及如何逐步提高程序的性能。主要内容如下:

  1. 优化进度:目前已经简化到了只剩下一个主要循环,其他部分已经清理或优化了。接下来需要做的工作是加载所有数据,虽然这可能会有些麻烦。计划从下周一开始进一步处理,预计会涉及到数据的重打包(repack),这个过程相对容易一些,方便解释和演示。

  2. 性能改进:经过优化后,游戏的速度已经恢复到实时运行状态。虽然优化的步骤并不复杂,主要是通过应用刚刚讲解的刚性变换(rigid transformations)来提升性能,这些变换并不依赖于特殊的经验或知识,任何人按照解释的步骤也能完成。

  3. 变换步骤的简化:优化过程通过将每次处理的像素数从1增加到4来加速计算,即通过扩展操作宽度。这类操作虽然一开始看起来可能会让人感到陌生或有些害怕,但实际上并不复杂,理解函数的基本用法后就能看懂。

  4. 代码优化的谨慎态度:在优化代码时,避免使用重载操作符,因为优化代码时对编译器行为的担忧。虽然重载操作符能使代码更简洁易读,但在进行性能优化时,担心编译器可能会做出不必要的优化,从而影响性能。为了确保代码按预期执行,选择了手动编写优化步骤,避免编译器做出不必要的调整。

  5. 小范围优化:优化的范围非常小,主要集中在像素填充等少数几个函数上,因此为了保证稳定性,手动写出这些优化代码是一个值得付出的成本,确保不会遇到编译器引发的意外问题。

总结来说,优化过程中通过对数据处理方式的调整,逐步提高了程序的运行速度。尽管优化的步骤不复杂,但在实际操作时,依然需要对编译器的行为保持谨慎,避免不必要的隐性优化。

如何处理多种 CPU 技术的内建函数?是否通过预处理器在不同的内建函数之间切换?另外,ASM 方面应该阅读 Mike Abrash 吗?

这段内容主要讨论了如何处理多种CPU技术的内在切换,以及如何应对与现代编译器相关的优化工作。以下是详细总结:

  1. 处理多种CPU技术的内在切换:为了适应多种技术,建议定义自有的内在版本(如自定义低级操作或特定的 SIMD 操作),并根据需要为特定的编译器或平台进行条件编译。这种做法的目的是确保代码能够根据不同平台的特点进行优化,并且可以覆盖不同的硬件架构。

  2. 关于阅读的建议:对于了解低级优化或CPU技术的人,建议不再关注像Mike Brash这样的经典资源,尤其是考虑到现代技术和指令集的发展。Mike Brash的书籍主要是针对Pentium架构的处理器,并且多讨论的是较旧的技术,今天的硬件优化要求已经发生了变化。虽然偶尔会有一些他提到的概念仍然有用,但它们在当前的优化工作中已经相对较少。

  3. Cindy优化的挑战:SIMD与传统的技术有很大不同,很多操作并不直接关联Mike Brash讨论的内容(如处理u和v通道)。现在的优化更多是关注如何确保数据在执行过程中能够按正确的顺序和对齐进行处理,而不是仅仅关注硬件特定的操作。因此,现代优化策略更多关注的是数据对齐、指令重排等更底层的细节。

总结来说,处理多种CPU技术时,主要通过定义自有的内在函数来适应不同的硬件架构,确保代码能够在不同平台上优化执行。对于学习资源,建议关注现代技术的优化策略,而不是依赖于旧有的书籍和技术。

我们从 385 周期降到了 123。是否可以应用 80%-20% 规则?你认为我们会降到 50 个周期吗?

这段内容讨论了优化计算循环周期的目标,以及如何判断是否能将每个像素的处理时间缩短到50个周期。以下是详细总结:

  1. 优化目标:目标是将当前的操作从大约85个周期降低到50个周期。虽然目前还没有做详细的估算,但从大致的分析来看,预计可以将循环的处理时间减少到每个像素50个周期。

  2. 操作计数与最大吞吐量:首先需要对所有操作进行计数,以了解最大吞吐量,并判断是否可以达到50个周期的目标。初步估计是,通过增加宽度、批量处理多个像素,每个像素的处理时间有可能降至50个周期。计算过程是通过将多个操作合并成一个操作来提高效率。

  3. 纹理获取与解包的挑战:虽然处理数学运算(如乘法和加法)是可以达到50个周期的目标,但纹理获取(texel fetch)和解包(unpack)操作可能是一个更大的挑战。需要进一步检查纹理操作的效率,看看是否能够将这些操作也优化到50个周期以内。

  4. 每像素处理的期望:如果能将总的周期数减少到200个周期,并且每次处理4个像素,那么每个像素的处理时间就有可能控制在50个周期以内。这个优化目标是完全可行的,前提是纹理获取和解包操作能同样得到优化。

总的来说,优化的关键是提高每个周期的吞吐量,尤其是在批量处理多个像素时,尽量减少不必要的操作,从而实现每像素50个周期的目标。尽管纹理操作仍然是一个不确定因素,但如果其他部分能够顺利优化,这个目标是有很大可能实现的。

我们使用 mmSquare 的方式,会不会计算参数两次?

在讨论中,提到使用 mmSquare 时,是否会计算相同的参数两次。虽然理论上不应该出现这种情况,但需要进行验证。编译器通常会很好地执行公共子表达式消除(CSE),以避免冗余计算。但这个问题是一个很好的提醒,值得进行检查,以确保没有不必要的重复计算。

调试器: 确定编译器是否在这些乘法操作中做了常见子表达式消除

在分析时,尝试查看代码中的平方计算,观察指令是否执行了重复计算。通过分析指令,发现 mmSquare 似乎在执行平方时确实对相同的值进行了两次计算。查看浮点值时,发现加载的常数与预期的不完全相同,而且值接近但并不完全相等。对这个情况感到困惑,因为常数的加载和平方操作的顺序不明确,编译器似乎做了一些优化或其他操作,但结果并不清晰。最终,无法准确判断编译器到底如何处理这些计算,可能需要更精通汇编语言的专家来解释编译器的优化和处理方式,判断其是否合理。
在这里插入图片描述

深入集中调查

在分析代码时,发现加载的寄存器值和预期值有些不同,但经过深入分析后确认,编译器使用了扩展寄存器(高位的寄存器),并且进行了正确的乘法操作。最初的困惑源于未注意到扩展寄存器,导致一度误解了编译器的操作。事实上,代码首先加载了所需的值并进行了乘法操作,同时使用了寄存器隐藏乘法的延迟,从而提高了处理效率。最终,代码按预期进行了平方和乘法操作,没有重复计算值,因此不会浪费任何周期。

这个过程展示了编译器如何通过智能使用寄存器和插入操作来优化性能,隐藏计算延迟,并且有效地进行多次操作,确保了整个流程的高效执行。

OpenCL 和其他 GPGPU 框架在优化中扮演什么角色?如果某些操作可以 SIMD 化,是不是可以在 GPU 上更广泛地处理?是否有一些工作负载更适合 CPU 和 SIMD?

在优化中,OpenCL 和其他 GPU 加速框架的作用取决于任务的特性。通常来说,优化的关键在于数据传输时间和任务是否适合 GPU 的处理模式。GPU 的设计使其非常适合流式处理,即能够直接移动数据、处理它并输出结果。

GPU 和 CPU 适用的任务类型:

  1. GPU 的适用场景

    • GPU 适合处理流式任务。这类任务有明确的输入和输出,且数据可以顺序流动,不需要大量的随机存取。
    • 典型的例子是图像处理、科学计算、机器学习等,其中的计算步骤可以并行执行,并且数据访问模式较为规则。
    • 对于不需要复杂控制流、重复读取和修改数据的任务,GPU 是理想的选择。
  2. CPU 的适用场景

    • CPU 更擅长处理通用任务,尤其是在处理涉及大量随机内存访问和复杂控制流的情况时。
    • 如果任务的执行涉及较多的“读取-修改-写”操作,特别是需要频繁访问和修改不同内存位置的情况下,GPU 的表现通常不如 CPU。
    • 例如,Xeon Phi 这样的处理器,包含多个核心,能够更高效地处理这些通用任务,其性能在通用计算任务中超越了 GPU。

总结:

  • 如果任务非常符合 GPU 的流式处理模型,即有简单、规律的输入输出流,并且不需要大量复杂控制流,那么 GPU(如使用 OpenCL)可能是一个不错的选择。
  • 但如果任务涉及复杂的内存操作、频繁的随机访问和较为复杂的控制流,则 CPU 或其他专用加速卡(如 Xeon Phi)可能会更合适。

为什么编译器中有优化选项,如果最终还要编写 SIMD 函数?

编译器中的优化选项并不是指编译器会自动将代码转化为 SIMD(单指令多数据)指令。优化的意义在于,编译器会尝试根据编写的代码生成更高效的汇编代码,这通常并不包括向量化(SIMD化)。向量化的过程需要编译器对代码有更深入的理解,才能合理地将其转化为并行执行的指令,而目前的技术通常还不支持自动完成这些复杂的优化。

优化在编译器中的作用是生成更好的汇编代码,这样的代码能够提高程序的执行效率,但它并不意味着会进行像自动向量化那样复杂的转换。自动向量化目前在编译器中并不完善,编译器无法像人类手动编写 SIMD 代码那样精确地控制向量化过程。

编译器的优化通常有两个方向:一个是尽量生成接近手写的高效汇编代码,另一个则是将代码以最简单的方式翻译成汇编,便于调试和查看。两者的区别在于,是否希望编译器在生成汇编代码时进行更多的思考,尝试提高效率,还是只做最基础的转换,便于程序员检查和调试。

目前,如果需要进行大规模的 SIMD 优化,编译器还不具备完全自动化的能力,程序员通常需要手动调整代码来进行向量化和其他并行优化。

你知道 _mm_setr_ps 内建函数(以及 _pd 等)吗?注意到 setr 中的 r 吗?它以更直观的顺序加载值

“逆序”加载值的顺序会使人困惑,因为直觉上看,逆序通常意味着大端存储(Big Endian)。然而,实际情况是,逆序意味着以小端存储(Little Endian)的顺序加载。这种存储方式在加载数据时的顺序与预期不同,因此容易让人混淆。

在使用这些特定的指令时,记住数据的加载顺序非常重要。即使知道了顺序,仍然可能因为不熟悉或记忆混乱,而难以准确判断加载的方式。对于这些操作,最好了解实际加载的数据顺序,避免出现理解上的偏差。

你认为什么时候会对渲染器进行多线程处理?

渲染的多线程处理可能不会很快实现,因为首先需要确保单线程的像素填充工作正常。因此,必须先集中精力优化和完善像素填充的单线程部分,确保其稳定且高效。一旦像素填充的优化完成,才可能考虑将渲染过程引入多线程。

可能是个误导性的问题,有没有办法重载运算符来使用 SIMD 指令?

重载运算符以使用SIMD指令是可行的,但存在一些问题。首先,编译器可能无法正确理解这种操作,导致性能损失。例如,使用向量运算而非标量运算可能会带来显著的周期惩罚。在某些情况下,使用SIMD指令会导致大量的额外周期开销,甚至可能占用函数周期的很大一部分,这对性能影响非常大。因此,通常不推荐将SIMD指令与重载运算符一起使用,因为编译器可能会出现困惑,并导致不必要的性能损失。

你还需要担心填充和对齐问题吗?我记得在 2000 年代中期,数据没有对齐时 SIMD 基本上是无效的

尽管在早期编程中数据对齐非常重要,甚至被认为是至关重要的,但如今的情况可能有所不同。虽然对齐仍然是一个需要关注的方面,但它似乎不像以前那样关键。例如,即使在没有专门进行数据对齐的情况下,SIMD指令的使用仍然表现得非常有效。这表明,虽然对齐可能仍然重要,但它并不是决定性的,性能优化并不完全依赖于数据对齐。

补充:我指的是“担心”,是否现在编译器在你“启用” SIMD 时会更自动地处理这些问题?

现在,编译器通常能够自动处理堆栈对齐,确保SIMD变量得到了正确对齐。因此,数据对齐问题不再像以前那样需要手动处理,编译器会自动确保相关变量不会发生对齐错误。

如果你移植到 ARM(例如 Raspberry Pi),会生成 NEON 汇编代码吗?我发现 GCC 在生成内建函数的正确代码方面表现不好(特别是在 Android 上)

在安卓平台上优化NEON指令集的经验较少。曾经优化过一次NEON代码,但并未深入优化,主要是通过编写代码生成器,将其输出到Android的ART(Android Runtime),生成NEON相关的汇编指令。虽然没有深入查看相关代码的细节,但这是一个尚未解决的问题,未来可能需要更多的工作来优化此部分。

如何知道某个操作是否会加速代码?尤其是当改动比较大的时候,且时间有限,我往往不敢做这样的优化,害怕引入错误

在面对代码重构和优化时,首先应该遵循一个清晰的过程:获取统计数据、找出代码瓶颈、构建代码模型,并根据这些信息评估代码的性能。通过这些步骤,可以确保优化工作的方向是正确的,且能逐步接近性能峰值。

对于优化代码时引入的bug,重要的是要有足够的信心和方法来处理。若无法确信重构代码时不会引入bug,应该正视这一问题。是否是因为编码不规范或是原始代码过于混乱,导致很难理解其正确性?解决方案之一是编写单元测试,通过这些测试来验证优化后的代码是否保持正确。

总之,不应害怕优化代码。优化不仅是提升性能的一部分,也是提高代码质量的一个过程。

你接下来想要转换成 SIMD 的是什么,如果我周末想练习的话?

如果想在周末进行练习,建议从简单的任务入手,比如优化绘制填充矩形的代码。在之前已经学习的基础上,可以尝试完全用SIMD优化这个绘制矩形的过程,除了最后处理填充部分(即“ShouldFill”部分)。这一部分我们尚未讲解,但其他内容都已经涵盖,因此可以独立完成。

具体来说,任务是对绘制矩形的代码进行SIMD优化,除了填充部分。填充的实现涉及到一些掩码操作,这部分暂时可以跳过。这个练习不涉及纹理加载,只是简单的绘制,因此可以专注于优化其他部分。

通过这种方式,可以巩固已学的知识,同时避免由于缺少特定知识而卡住。

能否使用 -Od 编译并展示 SIMD 在这方面的效果?

尽管可以进行编译并展示SIMD在优化中的效果,但这并不是一个有意义的衡量标准。当前的代码大约需要120个周期。如果切换到OD(优化的编译选项),虽然看起来会有所帮助,但问题在于,使用OD会插入大量额外的无用代码,这反而可能导致整体性能下降。因此,尽管OD在某些情况下可以提供帮助,但它的编译过程实际上会拖慢速度。

将简单的 CPU 学习作为练习(虽然是个大工程)如何?例如 Arduino 或其他古老的 CPU?我一直想学习 GBA 编程

学习汇编语言代码是一项很好的练习,可以帮助更熟悉底层编程,了解如何通过编写内在代码来优化程序,并学习这些代码是如何被编译的。虽然不需要过度投入,但重要的是找到能够激励自己学习的方式。无论是模拟编写一些简单的程序,还是通过其他有趣的方式,比如在Game Boy Advance上进行编程,只要能够帮助理解寄存器、内存、机器指令以及它们如何相互作用的思维模型,都是有益的。任何能够培养这种思维习惯的活动,都会帮助成为更好的程序员。如果优化一些代码或做类似的练习能激发兴趣,那就去做,最终会提升编程能力。

Game Boy Advance (简称 GBA) 是任天堂于2001年发布的便携式游戏机,继任了 Game Boy Color,属于 Game Boy 系列的一部分。它使用了一个16位的处理器,支持图形和音频功能,相比前代的 Game Boy 和 Game Boy Color,GBA 在性能上有显著提升。GBA 配备了一个2.9英寸的彩色液晶屏,并且能够向后兼容 Game Boy 和 Game Boy Color 的游戏。

GBA 有着丰富的游戏库,包括《口袋妖怪》、《马里奥兄弟》、《塞尔达传说》和《火焰纹章》等经典作品。它支持多种输入方式,比如 D-pad 和 AB 按钮,玩家可以使用这些按钮进行各种互动。

由于其小巧便捷和高质量的游戏,GBA 在便携式游戏机市场上占有一席之地,直到 2004 年 GBA SP(翻盖版)和 2005 年的 Nintendo DS 发布,标志着 GBA 的生命周期的结束。

让我们换个方式提问:你建议学习哪个 CPU,它既简单又足够有代表性,能帮助了解你在处理 CPU 时需要掌握的通用知识?

CPU 的选择并不是最重要的,关键是要理解代码与实际的 CPU 之间的关系。每年 CPU 都在不断发展,因此没有一个特定的 CPU 是必须学习的。重要的是,要培养起一种意识:无论是哪款 CPU,都有它的特定特征,代码最终都会在这些 CPU 上执行。

为了优化代码,首先需要理解每个 CPU 的基本特性,比如它的指令集、内存管理以及执行速度等。即使从未接触过某个特定的 CPU,也可以通过阅读相关资料来了解该 CPU 的执行机制,进而进行优化。

学习任何一种 CPU 都能帮助建立这种思维模式。之后,每次遇到新的 CPU 时,都可以通过学习它的特性,继续提升代码优化能力。因为 CPU 是不断更新和变化的,所以不能依赖某个固定的模型,而是要根据实际使用的 CPU 去学习并理解其细节。

你会优化游戏代码吗?

游戏玩法代码可能不会进行太多优化,因为这部分工作量不会占用大部分时间。然而,可能会对碰撞检测等方面进行优化。

总结本周的优化工作

本周的内容已接近尾声,虽然我们没有深入检查所有内容,但调试代码现在的运行速度与优化版本相当,甚至达到了实时运行的效果。目前运行在30帧每秒左右,这已经是一个相当不错的进展。

尽管优化和其他部分(如打包和解包)的工作还有很多需要完成,但经过五小时的编程,已经从无法运行到现在能够稳定运行,进步是显著的。

接下来,将会进行更深入的优化,重点会是处理更复杂的内容,比如在下周讨论的向量和旋转问题,虽然这些任务稍微复杂,但并不是非常困难,只是会显得繁琐。

下周的内容会涉及更高级的优化,并探讨一些数学运算的处理方法。我们将继续改进软件渲染路径,以确保性能提升。若有兴趣可以提前下载源代码,并跟随一同进行练习。


http://www.ppmy.cn/news/1574155.html

相关文章

Innovus中快速获取timing path逻辑深度的golden脚本

在实际项目中我们经常会遇到一条timing path级数特别多&#xff0c;可能是一两页都翻不完。此时&#xff0c;我们大都需要手工去数这条path上到底有哪些是设计本身的逻辑&#xff0c;哪些是PR工具插入的buffer和inverter。 数字IC后端手把手培训教程 | Clock Gating相关clock …

HTTP SSE 实现

参考&#xff1a; SSE协议 SSE技术详解&#xff1a;使用 HTTP 做服务端数据推送应用的技术 一句概扩 SSE可理解为&#xff1a;服务端和客户端建立连接之后双方均保持连接&#xff0c;但仅支持服务端向客户端推送数据。推送完毕之后关闭连接&#xff0c;无状态行。 下面是基于…

ubuntu docker 安装 deepseek anythingllm/openwebui教程

全新服务器安装起始&#xff1a; 1. 安装ubuntu到服务器中 2. 安装docker 安装教程 ubuntu 安装 docker详细教程_ubuntu安装教程docker-CSDN博客 3. 安装 ollama docker pull ollama/ollama 3.1 创建 存储目录 &#xff08;示例放在/home/ollama中&#xff09; cd /home/ …

1.15作业

1 &#xff08;函数&#xff1a;获取参数中的正确格式的host&#xff0c;要求符合红色的4个ip&#xff09; ?urlhttp://127.0.0.0/flag.php/ 2 直接导出http/tcp协议&#xff0c;十六进制流&#xff1a;HEX转字符 十六进制转字符 hex gb2312 gbk utf8 汉字内码转换 - The X 在…

非线性最小二乘拟合问题

引入 回顾经典的SLAM模型&#xff0c;由观测方程和运动方程组成&#xff1a; { x k f ( x k − 1 , u k ) w k z k j h ( y j , x k ) v k \begin{equation} \left\{ \begin{aligned} & x_kf(x_{k-1},u_k) w_k\\ & z_k^{j} h(y_j,x_k)v_k \end{aligned} \right.…

适用于复杂背景的YOLOv8改进:基于DCN的特征提取能力提升研究

文章目录 1. YOLOv8的性能瓶颈与改进需求1.1 YOLOv8的优势与局限性1.2 可变形卷积&#xff08;DCN&#xff09;的优势 2. DCN在YOLOv8中的应用2.1 DCN的演变与YOLOv8的结合2.2 将DCN嵌入YOLOv8的结构中2.2.1 DCNv1在YOLOv8中的应用2.2.2 DCNv2与DCNv3的优化 2.3 实验与性能对比…

SpringBoot自定义分布式配置同步Starter实现

以下将详细介绍如何基于 Spring、Spring Boot、Spring Cloud、Nacos、RabbitMQ 实现分布式系统中基于远程事件的服务配置同步&#xff0c;并封装成基础框架&#xff0c;方便其他开发人员和服务调用。 整体思路 我们的目标是构建一个配置同步基础框架&#xff0c;主要包含以下…

Spring Boot中使用Flyway进行数据库迁移

文章目录 概要Spring Boot 集成 FlywayFlyway 其他用法bug错误Flyway版本不兼容数据库存在表了Flyway 的校验和&#xff08;Checksum&#xff09;不匹配 概要 在 Spring Boot 项目开发中&#xff0c;数据库的变更不可避免。手动执行 SQL 脚本不仅容易出错&#xff0c;也难以维…