如何提高float lerp功能的速度

How to improve the speed of the float lerp function?

本文关键字:速度 功能 lerp 何提高 float      更新时间:2023-10-16

最近我正在编写一个软光栅渲染器,但它的速度真的很慢。通过性能测试,我发现float lerp函数是瓶颈。如何提高此功能的速度?使用simd?知道吗?

inline float MathUtil::Lerp(float x1, float x2, float t)
{
    return x1 + (x2 - x1)*t;
}
//lerp vector
ZCVector MathUtil::Lerp(const ZCVector& v1, const ZCVector& v2, float t)
{
    return ZCVector(
        Lerp(v1.x, v2.x, t),
        Lerp(v1.y, v2.y, t),
        Lerp(v1.z, v2.z, t),
        v1.w
    );
}
//lerp ZCFLOAT2
ZCFLOAT2 MathUtil::Lerp(const ZCFLOAT2& v1, const ZCFLOAT2& v2, float t)
{
    return ZCFLOAT2(
        Lerp(v1.u, v2.u, t),
        Lerp(v1.v, v2.v, t)
    );
}
//lerp ZCFLOAT3
ZCFLOAT3 MathUtil::Lerp(const ZCFLOAT3& v1, const ZCFLOAT3& v2, float t)
{
    return ZCFLOAT3(
        Lerp(v1.x, v2.x, t),
        Lerp(v1.y, v2.y, t),
        Lerp(v1.z, v2.z, t)
    );
}
//lerp VertexOut
VertexOut MathUtil::Lerp(const VertexOut& v1, const VertexOut& v2, float t)
{
    return VertexOut(
        Lerp(v1.posTrans, v2.posTrans, t),
        Lerp(v1.posH, v2.posH, t),
        Lerp(v1.tex, v2.tex, t),
        Lerp(v1.normal, v2.normal, t),
        Lerp(v1.color, v2.color, t),
        Lerp(v1.oneDivZ, v2.oneDivZ, t)
    );
}

VertexOut的结构:

class VertexOut
{
public:
    ZCVector posTrans;
    ZCVector posH;
    ZCFLOAT2 tex;
    ZCVector normal;
    ZCFLOAT3 color;
    float oneDivZ;
}

scanlinefill函数来填充三角形,每个顶点都需要使用lerp函数,所以它会被调用很多次。

void Tiny3DDeviceContext::ScanlineFill(const VertexOut& left, const VertexOut& right,  int yIndex)
{
    float dx = right.posH.x - left.posH.x;
    for (float x = left.posH.x; x <= right.posH.x; x += 0.5f)
    {
        int xIndex = static_cast<int>(x + .5f);
        if(xIndex >= 0 && xIndex < m_pDevice->GetClientWidth())
        {
            float lerpFactor = 0;
            if (dx != 0)
            {
                lerpFactor = (x - left.posH.x) / dx;
            }

            float oneDivZ = MathUtil::Lerp(left.oneDivZ, right.oneDivZ, lerpFactor);
            if (oneDivZ >= m_pDevice->GetZ(xIndex,yIndex))
            {
                m_pDevice->SetZ(xIndex, yIndex, oneDivZ);
                //lerp get vertex
                VertexOut out = MathUtil::Lerp(left, right, lerpFactor);
                out.posH.y = yIndex;
                m_pDevice->DrawPixel(xIndex, yIndex, m_pShader->PS(out));
            }           
        }   
    }
}

此循环结构可能运行lerp的次数是所需次数的两倍:

for (float x = left.posH.x; x <= right.posH.x; x += 0.5f) {
      int xIndex = static_cast<int>(x + .5f);
      ...
}

相反,(更准确地说(,通过递增整数xIndex进行循环,并为每个xIndex计算正确的float x


这可能会自动向量化,但您必须检查编译器输出,看看发生了什么。希望用out.posH.y = yIndex;覆盖的Lerp在丢弃结果后得到优化。如果没有,您可能会通过制作一个不做Lerp的包装器函数来获得加速。


通过使用数组结构方法,而不是保持结构的所有内容连续的AoS方法,可以使它对SIMD更友好。然而,您以相同的方式排列多个元素,因此它可能会使用两个标量和一个向量Lerp自动向量化。

有关SIMD内容的一些指南,请参阅sse标签wiki,其中包括一个指向这组非常好的初学者/中级幻灯片的链接。


你可能还可以改变其他事情,特别是对代码进行更大的重组,以减少整体工作。这种优化通常比使用SIMD来有效地应用现代CPU的暴力更能给你带来更大的加速。

同时做这两件事来增加加速是真正让事情变得快速的原因。

缓存未命中和内存带宽瓶颈通常是一个巨大的因素,因此优化访问模式可以产生很大的影响。

如果您想了解更多底层细节,请参阅Agner Fog的优化指南。他有一个C++优化指南,但大多数好东西都是关于x86 asm的。(另请参阅x86标记wiki(。但请记住,在寻找高级优化之后,这种低级优化只是一个好主意。