为什么``精确的预选赛''不生效

Why does `precise` qualifier not take effect?

本文关键字:预选赛 为什么      更新时间:2023-10-16

我正在尝试改善亨利·塔斯勒(Henry Thasler(的GLSL实现双单旋算术(从他的GLSL mandelbrot演示(,以可靠地在Linux上可靠地工作。我最近了解到,自OpenGL 4.0(§4.7规格中的精确限定符(或使用GL_ARB_gpu_shader5扩展名(SPEC(,我们可以使用precise预选赛来使计算遵循精确的算术操作顺序GLSL源。

但是以下尝试似乎没有任何改进:

#version 330
#extension GL_ARB_gpu_shader5 : require
vec2 ds_add(vec2 dsa, vec2 dsb)
{
    precise float t1 = dsa.x + dsb.x;
    precise float e = t1 - dsa.x;
    precise float t2 = ((dsb.x - e) + (dsa.x - (t1 - e))) + dsa.y + dsb.y;
    precise vec2 dsc;
    dsc.x = t1 + t2;
    dsc.y = t2 - (dsc.x - t1);
    return dsc;
}

结果与未添加precise相同。我已经检查了算法本身是正确的:它在Intel Core i7-4765T内置图形上的工作原样(即使没有precise(,并且如果我隐藏一些变量以抑制优化,那么Nvidia也会给出正确的结果。这是我抑制优化的方式:

#version 330
#define hide(x) ((x)*one)
uniform float one=1;
vec2 ds_add(vec2 dsa, vec2 dsb)
{
    float t1 = dsa.x + dsb.x;
    float e = hide(t1) - dsa.x;
    float t2 = ((dsb.x - e) + (dsa.x - (t1 - e))) + dsa.y + dsb.y;
    vec2 dsc;
    dsc.x = t1 + t2;
    dsc.y = t2 - (hide(dsc.x) - t1);
    return dsc;
}

因此,显然,我错误地使用了precise预选赛。但是这里到底有什么问题?

供参考,我正在使用Nvidia geforce GTX 750TI与二进制NVIDIA驱动程序390.116。这是完整的C 测试:

#include <cmath>
#include <vector>
#include <string>
#include <limits>
#include <iomanip>
#include <iostream>
// glad.h is generated by the following command:
// glad --out-path=. --generator=c --omit-khrplatform --api="gl=3.3" --profile=core --extensions=
#include "glad/glad.h"
#include <GL/freeglut.h>
#include <glm/glm.hpp>
using glm::vec4;
GLuint vao, vbo;
GLuint texFBO;
GLuint program;
GLuint fbo;
int width=1, height=2;
void printShaderOutput(int texW, int texH)
{
    glActiveTexture(GL_TEXTURE0);
    glBindTexture(GL_TEXTURE_2D, texFBO);
    std::vector<vec4> data(texW*texH);
    glGetTexImage(GL_TEXTURE_2D, 0, GL_RGBA, GL_FLOAT, data.data());
    std::cout << "a,b,sum,relError(sum),noten";
    for(int i=0;i<width;++i)
    {
        const auto a=double(data[i+width*0].x)+double(data[i+width*0].y);
        const auto b=double(data[i+width*0].z)+double(data[i+width*0].w);
        const auto sum=double(data[i+width*1].x)+double(data[i+width*1].y);
        const auto trueSum=a+b;
        const auto sumErr=(sum-trueSum)/trueSum;
        std::cout << std::setprecision(std::numeric_limits<double>::max_digits10)
                  << a << ',' << b << ','
                  << sum << ','
                  << std::setprecision(3)
                  << sumErr << ','
                  << (std::abs(sumErr)>1e-14 ? "WARN" : "OK")
                  << 'n';
    }
    std::cout.flush();
}
GLuint makeShader(GLenum type, std::string const& srcStr)
{
    const auto shader=glCreateShader(type);
    const GLint srcLen=srcStr.size();
    const GLchar*const src=srcStr.c_str();
    glShaderSource(shader, 1, &src, &srcLen);
    glCompileShader(shader);
    GLint status=-1;
    glGetShaderiv(shader, GL_COMPILE_STATUS, &status);
    assert(glGetError()==GL_NO_ERROR);
    assert(status);
    return shader;
}
void loadShaders()
{
    program=glCreateProgram();
    const auto vertexShader=makeShader(GL_VERTEX_SHADER, 1+R"(
#version 330
in vec4 vertex;
void main() { gl_Position=vertex; }
)");
    glAttachShader(program, vertexShader);
    const auto fragmentShader=makeShader(GL_FRAGMENT_SHADER, 1+R"(
#version 330
#extension GL_ARB_gpu_shader5 : require
vec2 ds_add(vec2 dsa, vec2 dsb)
{
    precise float t1 = dsa.x + dsb.x;
    precise float e = t1 - dsa.x;
    precise float t2 = ((dsb.x - e) + (dsa.x - (t1 - e))) + dsa.y + dsb.y;
    precise vec2 dsc;
    dsc.x = t1 + t2;
    dsc.y = t2 - (dsc.x - t1);
    return dsc;
}
uniform vec2 a, b;
out vec4 color;
void main()
{
    if(gl_FragCoord.y<1)   // first row
        color=vec4(a,b);
    else if(gl_FragCoord.y<2)   // second row
        color=vec4(ds_add(a,b),0,0);
}
)");
    glAttachShader(program, fragmentShader);
    glLinkProgram(program);
    GLint status=0;
    glGetProgramiv(program, GL_LINK_STATUS, &status);
    assert(glGetError()==GL_NO_ERROR);
    assert(status);
    glDetachShader(program, fragmentShader);
    glDeleteShader(fragmentShader);
    glDetachShader(program, vertexShader);
    glDeleteShader(vertexShader);
}
void setupBuffers()
{
    glGenVertexArrays(1, &vao);
    glBindVertexArray(vao);
    glGenBuffers(1, &vbo);
    glBindBuffer(GL_ARRAY_BUFFER, vbo);
    const GLfloat vertices[]=
    {
        -1, -1,
         1, -1,
        -1,  1,
         1,  1,
    };
    glBufferData(GL_ARRAY_BUFFER, sizeof vertices, vertices, GL_STATIC_DRAW);
    constexpr GLuint attribIndex=0;
    constexpr int coordsPerVertex=2;
    glVertexAttribPointer(attribIndex, coordsPerVertex, GL_FLOAT, false, 0, 0);
    glEnableVertexAttribArray(attribIndex);
    glBindVertexArray(0);
}
bool init()
{
    if(!gladLoadGL())
    {
        std::cerr << "Failed to initialize GLADn";
        return false;
    }
    if(!GLAD_GL_VERSION_3_3)
    {
        std::cerr << "OpenGL 3.3 not supportedn";
        return false;
    }
    glGenTextures(1, &texFBO);
    glGenFramebuffers(1,&fbo);
    loadShaders();
    setupBuffers();
    glViewport(0,0,width,height);
    glBindTexture(GL_TEXTURE_2D,texFBO);
    glTexImage2D(GL_TEXTURE_2D,0,GL_RGBA32F,width,height,0,GL_RGBA,GL_UNSIGNED_BYTE,nullptr);
    glBindTexture(GL_TEXTURE_2D,0);
    glBindFramebuffer(GL_FRAMEBUFFER,fbo);
    glFramebufferTexture2D(GL_FRAMEBUFFER,GL_COLOR_ATTACHMENT0,GL_TEXTURE_2D,texFBO,0);
    const auto status=glCheckFramebufferStatus(GL_FRAMEBUFFER);
    assert(status==GL_FRAMEBUFFER_COMPLETE);
    glBindFramebuffer(GL_FRAMEBUFFER,0);
    return true;
}
void display()
{
    const static bool inited=init();
    if(!inited) std::exit(1);
    glBindFramebuffer(GL_FRAMEBUFFER,fbo);
    glUseProgram(program);
#define SPLIT_DOUBLE_TO_FLOATS(x) GLfloat(x),GLfloat(x-GLfloat(x))
    glUniform2f(glGetUniformLocation(program,"a"),SPLIT_DOUBLE_TO_FLOATS(3.1415926535897932));
    glUniform2f(glGetUniformLocation(program,"b"),SPLIT_DOUBLE_TO_FLOATS(2.7182818284590452));
    glUniform1f(glGetUniformLocation(program,"rtWidth"),width);
    glBindVertexArray(vao);
    glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
    glBindVertexArray(0);
    printShaderOutput(width, height);
    std::exit(0);
    glFinish();
}
int main(int argc, char** argv)
{
    glutInitContextVersion(3,3);
    glutInitContextProfile(GLUT_CORE_PROFILE);
    glutInit(&argc, argv);
    glutInitDisplayMode(GLUT_RGB);
    glutInitWindowSize(width, height);
    glutCreateWindow("Test");
    glutDisplayFunc(display);
    glutMainLoop();
}

在不同情况下,我能够从GLSL程序二进制文件中提取NVFP5.0组件:

  • 没有hide的天真案例,没有precise
!!NVfp5.0
OPTION NV_internal;
OPTION NV_bindless_texture;
PARAM c[2] = { program.local[0..1] };
TEMP R0;
TEMP T;
TEMP RC, HC;
OUTPUT result_color0 = result.color;
SLT.F R0.x, fragment.position.y, {1, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
MOV.F result_color0.xy, c[0];
MOV.F result_color0.zw, c[1].xyxy;
ELSE;
SLT.F R0.x, fragment.position.y, {2, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
ADD.F R0.y, -c[0].x, c[0].x;
ADD.F R0.x, -c[1], c[1];
ADD.F R0.x, R0, R0.y;
ADD.F R0.x, R0, c[0].y;
ADD.F R0.y, R0.x, c[1];
ADD.F R0.x, c[0], c[1];
ADD.F result_color0.x, R0, R0.y;
ADD.F result_color0.y, R0, -R0;
MOV.F result_color0.zw, {0, 0, 0, 0}.x;
ENDIF;
ENDIF;
END
  • 使用precise的情况(请注意,除了"指令"中的.PREC后缀之外,没有任何变化(:
!!NVfp5.0
OPTION NV_internal;
OPTION NV_bindless_texture;
PARAM c[2] = { program.local[0..1] };
TEMP R0;
TEMP T;
TEMP RC, HC;
OUTPUT result_color0 = result.color;
SLT.F R0.x, fragment.position.y, {1, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
MOV.F result_color0.xy, c[0];
MOV.F result_color0.zw, c[1].xyxy;
ELSE;
SLT.F R0.x, fragment.position.y, {2, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
ADD.F.PREC R0.y, -c[0].x, c[0].x;
ADD.F.PREC R0.x, -c[1], c[1];
ADD.F.PREC R0.x, R0, R0.y;
ADD.F.PREC R0.x, R0, c[0].y;
ADD.F.PREC R0.y, R0.x, c[1];
ADD.F.PREC R0.x, c[0], c[1];
ADD.F.PREC result_color0.x, R0, R0.y;
ADD.F.PREC result_color0.y, R0, -R0;
MOV.F result_color0.zw, {0, 0, 0, 0}.x;
ENDIF;
ENDIF;
END
  • hide的情况,确实有效,并且显然具有不同的算术操作:
!!NVfp5.0
OPTION NV_internal;
OPTION NV_bindless_texture;
PARAM c[3] = { program.local[0..2] };
TEMP R0, R1;
TEMP T;
TEMP RC, HC;
OUTPUT result_color0 = result.color;
SLT.F R0.x, fragment.position.y, {1, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
MOV.F result_color0.xy, c[1];
MOV.F result_color0.zw, c[2].xyxy;
ELSE;
SLT.F R0.x, fragment.position.y, {2, 0, 0, 0};
TRUNC.U.CC HC.x, R0;
IF NE.x;
ADD.F R0.x, c[1], c[2];
MAD.F R0.y, R0.x, c[0].x, -c[1].x;
ADD.F R0.z, R0.x, -R0.y;
ADD.F R0.z, -R0, c[1].x;
ADD.F R0.y, -R0, c[2].x;
ADD.F R0.y, R0, R0.z;
ADD.F R0.y, R0, c[1];
ADD.F R0.y, R0, c[2];
ADD.F R1.x, R0, R0.y;
MAD.F R0.x, R1, c[0], -R0;
MOV.F R1.zw, {0, 0, 0, 0}.x;
ADD.F R1.y, R0, -R0.x;
MOV.F result_color0, R1;
ENDIF;
ENDIF;
END

我从未使用过精确的自己,尽管您可能会从这里学习Opencl或Cuda受益。

无论如何,您的GLSL版本为3.30,与OpenGL 3.3绑在一起。通过扩展名可以避免确切的预选赛,但是如果可以的话,我总是会尝试使用OpenGL的内置功能。

扩展程序可能不会以相同的方式实现,我建议您尝试至少使用GLSL版本4.0,理想情况下是最新的OpenGL/GLSL版本。

有时,如果没有人使用它们,这些旧扩展可以在新的GPU上有回归。

GPU编译器往往具有优化的更加宽松。您可能会从看到编译的着色器的输出中受益,可能会有某种方法可以通过GLSL查看来自NVIDIA编译器的PTX组件输出。使用CUDA,您绝对可以预览组件输出,以确保编译器未重新订购操作。

规格称MAD是授予预选赛的主要原因 - 它将迫使编译器不使用MAD指令。对于精确的预选赛的加法/减法也许很少进行测试。

如果hide为您解决了问题,最好只称其为一天,我怀疑确切的预选赛已在GLSL方面进行了彻底检查。我强烈推荐Cuda或Opencl为此,如果您也想快速显示纹理,则可以使用CL-GL Interop,这并不痛苦。

确切的预选赛可确保没有重新订购操作,但不会提及不影响订购的优化。使用时,AMD似乎只是关闭了优化。NVIDIA仍然可能采用优化,这些优化影响您的结果,而结果与操作顺序无关,而与进行添加的特定优化有关。

precise float t1 = dsa.x + dsb.x;
precise float e = t1 - dsa.x;

这可能将e作为简单的dsb.x计算。编译器可能仍在添加不影响操作顺序的优化,因为这就是规格保证的。除了重新订购会影响这一结果的操作外,我想不出其他任何事情,但是我在这里不是专家。

要注意的另一件事是,根据我对规格的粗略阅读,DS_ADD的结果也可能需要存储到精确的变量中,以便精确计算。该函数只能在NVIDIA上插入(至少在历史上具有更好的优化(,因此我想编译器可以执行内部内部,然后如果将结果存储到非专有变量中,则所有现有的精确限定符都是被忽略。

着色器没有错。DS_ADD((代码没有在编译时可以合并的任何操作。通常添加并乘以/分割合并。但是您的代码仅添加操作。

更新:

  1. 在情况下,所有变量在计算过程中存储在GPU寄存器中。寄存器的操作顺序不取决于代码或编译器。它甚至不仅取决于硬件。这取决于当前在GPU中运行操作。

  2. 寄存器之间的浮点操作的精度并不是严格的32位。那通常更高。GPU的实际精度是商业秘密。X86 FPU的实际精度是80位或128位,尽管变量存储在32位存储器中。

  3. 但是,GPU不是为非常精确的计算而设计的。该算法的作者知道这一点,并实现了32位浮子的双重思想对。如果您需要提高精度,则必须使用长双倍的双倍频率使用32位浮子。简单的"精确"无济于事。