从不确定值到未指定值的有效转换

时间:2017-12-02 18:50:59

标签: c undefined-behavior c99 c11

有时在C语言中,有必要从部分写入的数组中读取可能写入的项目,这样:

  1. 如果该项目已被写入,则读取将产生实际写入的值,并且

  2. 如果项目没有被写入,则读取会将未指定的位模式转换为适当类型的值,没有副作用。

  3. 有各种各样的算法,从头开始寻找解决方案的成本很高,但验证提出的解决方案很便宜。如果一个数组为所有已找到它们的情况保留解决方案,并为其他情况保留任意位模式,读取数组,测试它是否包含有效解决方案,并且只有当数组中的那个没有时才慢慢计算解决方案。有效,可能是一个有用的优化。

    如果尝试读取类似uint32_t类型的非写入数组元素的尝试可以保证始终产生适当类型的值,那么这种方法将非常简单直接。即使该要求仅适用于unsigned char,它仍然可行。不幸的是,编译器有时表现得好像读取不确定值,即使类型为unsigned char,也可能会产生一些不一致的行为作为该类型的值。此外,缺陷报告中的讨论表明涉及不确定值的操作会产生不确定的结果,因此即使给出unsigned char x, *p=&x; unsigned y=*p & 255; unsigned z=(y < 256);之类的内容,z也可能会收到值0

    据我所知,功能:

    unsigned char solidify(unsigned char *p)
    {
      unsigned char result = 0;
      unsigned char mask = 1;
      do
      {
        if (*p & mask) result |= mask;
        mask += (unsigned)mask; // Cast only needed for capricious type ranges
      } while(mask);
      return result;
    }
    
    只要识别的存储可以作为该类型访问,即使它恰好保持Indeterminate Value,

    也可以保证始终在类型unsigned char的范围内产生值。然而,这种方法似乎相当缓慢和笨重,因为获得所需效果所需的机器代码通常应该等同于返回x

    标准是否有任何更好的方法可以保证始终产生unsigned char范围内的值,即使源值是不确定的?

    附录

    除了其他方面,当使用部分编写的数组和结构执行I / O时,固件值的能力是必要的,在这种情况下,没有什么可以关心从未设置的部分输出什么位。无论标准是否要求fwrite可以与部分写入的结构或数组一起使用,我会认为可以这种方式使用的I / O例程(为没有设置的部分写入任意值)在这种情况下,质量要高于那些可能会跳过轨道的质量。

    我担心的主要是防止不太可能在危险组合中使用的优化,但是当编译器变得越来越多时,可能会出现这种优化&#34;聪明的&#34;。

    类似的问题:

    unsigned char solidify_alt(unsigned char *p)
    { return *p; }
    

    编译器可能会将一个可能很麻烦但可以单独容忍的优化结合起来,其中一个优点可以隔离,但与第一个结合起来是致命的:

    1. 如果函数传递了unsigned char的地址,该地址已经过优化,例如一个32位寄存器,如上所述的函数可以盲目地返回该寄存器的内容而不将其剪切到0-255的范围。要求呼叫者手动剪辑这些功能的结果将是烦人的,但如果这是唯一的问题,则可以存活。不幸的是...

    2. 由于上述功能将始终&#34;总是&#34;返回值0-255,编译器可以省略任何&#34;下游&#34;试图将值掩盖到该范围内的代码,检查它是否在外面,或者执行与0-255范围之外的值无关的事情。

    3. 某些I / O设备可能要求希望写入八位字节的代码对I / O寄存器执行16位或32位存储,并且可能要求8位包含要写入的数据而其他位保持某种模式。如果任何其他位设置错误,它们可能会严重故障。考虑一下代码:

      void send_byte(unsigned char *p, unsigned int n)
      {
        while(n--)
          OUTPUT_REG = solidify_alt(*p++) | 0x0200;
      }
      void send_string4(char *st)
      {
        unsigned char buff[5]; // Leave space for zero after 4-byte string
        strcpy((char*)buff, st);
        send_bytes(buff, 4);
      }
      

      withsedended语义send_string4(&#34; Ok&#34;);应该发出一个&#39;一个&#39;一个零字节和一个任意值0-255。由于代码使用solidify_alt而不是solidify,编译器可以合法地将其转换为:

      void send_string4(char *st)
      {
        unsigned buff0, buff1, buff2, buff3;
        buff0 = st[0]; if (!buff0) goto STRING_DONE;
        buff1 = st[1]; if (!buff1) goto STRING_DONE;
        buff2 = st[2]; if (!buff2) goto STRING_DONE;
        buff3 = st[3];
       STRING_DONE:
        OUTPUT_REG = buff0 | 0x0200;
        OUTPUT_REG = buff1 | 0x0200;
        OUTPUT_REG = buff2 | 0x0200;
        OUTPUT_REG = buff3 | 0x0200;
      }
      

      具有以下效果:OUTPUT_REG可以接收位设置在适当范围之外的值。即使输出表达式更改为((unsigned char)solidify_alt(*p++) | 0x0200) & 0x02FF),编译器仍然可以简化它以产生上面给出的代码。

      标准的作者没有要求编译器生成的自动变量初始化,因为如果在语义上不需要这样的初始化,它会使代码变慢。我不认为他们打算在所有位模式同样可以接受的情况下,程序员必须手动初始化自动变量。

      注意,顺便说一句,在处理短数组时,初始化所有值将是便宜的并且通常是个好主意,并且当使用大型数组时,编译器不太可能强加上述&#34;优化&#34 ;。在数组足够大以至于成本重要的情况下省略初始化会使程序的正确操作依赖于&#34;希望&#34;。

2 个答案:

答案 0 :(得分:1)

这不是答案,而是延伸评论。

立即解决方案是让编译器提供内置的,例如assume_initialized(variable [, variable ... ]*),它不生成机器代码,只是让编译器处理指定变量的内容(标量或数组) 已定义但未知

使用另一个编译单元中定义的虚函数可以实现类似的效果,例如

void define_memory(void *ptr, size_t bytes)
{
    /* Nothing! */
}

并调用它(例如define_memory(some_array, sizeof some_array)),以阻止编译器将数组中的值视为不确定;这是有效的,因为在编译时,编译器无法确定未指定的值,因此必须考虑它们的指定(已定义但未知)。

不幸的是,这会导致严重的性能损失。即使函数体是空的,调用本身也会对性能产生影响。然而,更糟糕的是对代码生成的影响:因为数组是在单独的编译单元中访问的,所以数据实际上必须以数组形式驻留在内存中,因此通常会生成额外的内存访问,并限制编译器的优化机会。特别是,即使是一个小数组也必须存在,并且不能隐含或完全驻留在机器寄存器中。

我已经尝试了一些架构(x86-64)和编译器(GCC)特定的解决方法(使用扩展的内联汇编来欺骗编译器以相信值已定义但未知(未指定,而不是不确定),没有生成实际的机器代码 - 因为这不需要任何机器代码,只需对编译器处理数组/变量的方式进行一些小调整,但成功率几乎为零。

现在,我写这篇评论的根本原因。

多年前,从事数值计算代码并将性能与Fortran 95中的类似实现进行比较,我发现缺少memrepeat(ptr, first, bytes)函数:memmove()memcpy()的对应关系first 1}},它会在ptrptr+first 重复 ptr+bytes-1个字节,最多为memmove()。与ptr类似,它可以处理数据的存储表示,因此即使ptr+first double nums[7] = { 7.0, 6.0, 5.0, 4.0, 3.0, 2.0, 1.0 }; memrepeat(nums, 2 * sizeof nums[0], sizeof nums); 包含陷阱表示,也不会实际触发陷阱。

主要用例是初始化具有浮点数据(一维,多维或具有浮点成员的结构)的数组,通过初始化第一个结构或一组值,然后简单地重复存储模式整个阵列。这是数值计算中非常常见的模式。

例如,使用

    double nums[7] = { 7.0, 6.0, 7.0, 6.0, 7.0, 6.0, 7.0 };

产量

memsetall(data, size, count)

(如果编译器被定义为例如size,其中count是重复存储单元的大小,并且count-1,编译器可能会更好地优化操作。存储单元的总数(实际上复制了memsetall()单元)。特别是,这允许轻松实现使用非时间存储来复制,从初始存储单元读取。另一方面,{{1} }}只能复制与memrepeat()不同的完整存储单元,因此memsetall(nums, 2 * sizeof nums[0], 3);会使nums[]中的第7个元素保持不变 - 即,在上面的示例中,它会产生{{1} }}。)

虽然您可以简单地实现{ 7.0, 6.0, 7.0, 6.0, 7.0, 6.0, 1.0 }memrepeat(),但即使针对特定的体系结构和编译器进行优化,也很难编写可移植的优化版本。

特别是,使用memsetall()(或memcpy())的基于循环的实现在编译时会产生非常低效的代码。 GCC,因为编译器无法将函数调用模式合并为单个操作。

大多数编译器通常使用内部,目标和用例优化版本内联memmove()memcpy(),并为此类memmove()和/或{{1}执行此操作函数将使其可移植。在Linux on x86-64上,GCC内联已知大小的调用,但保留函数调用只能在运行时知道大小。

我确实尝试将其推向上游,在各种邮件列表上进行了一些私人和一些公开讨论。回应是亲切的,但很清楚:没有办法将这些功能包含在编译器中,除非它首先由某人标准化,或者你足够激发其中一个核心开发人员的兴趣,以便他们自己尝试。< / p>

因为C标准委员会只关心履行其企业赞助商的商业利益,所以没有机会将标准化的东西变成ISO C.(如果有的话,我们真的应该推动POSIX的基本功能memrepeat()memsetall()getline()首先包含 ;它们对我们可以教给新C程序员的代码有更大的积极影响。 )

这也没有引起核心GCC开发者的兴趣,所以在那一点上,我失去了尝试将其推向上游的兴趣。

如果我的经验是典型的 - 并且与少数人讨论它看起来确实如此 - OP和其他担心此类事情的人将更好地利用他们的时间来找到编译器/体系结构特定的解决方法,而不是点排除标准中的不足之处:标准已经丢失,那些人不在乎。

最好把时间和精力花在你可以实现的事情上,而不必与风车作斗争。

答案 1 :(得分:0)

我认为这很清楚。 C11 3.19.2

  

不确定价值
  要么是未指定的值,要么是陷阱   表示

周期。除上述两种情况外,它不能是其他任何东西。

因此unsigned z=(y < 256)之类的代码永远不会返回0,因为示例中的x不能保存大于255的值。根据字符类型的表示形式,6.2 .6,unsigned char不允许包含填充位或陷阱表示。

其他类型,在异国情调的系统上,理论上可以保持超出其范围的值,填充位和陷阱表示。

在真实世界的系统中,极有可能使用二进制补码,陷阱表示不存在。因此,不确定值只能是未指定的。未指定,未定义!有一个神话说“读取不确定的值始终是未定义的行为”。保存陷阱表示和一些其他特殊情况,这不是真的,see this。这仅仅是未指定的行为

未指定的行为并不意味着编译器可以运行破坏并做出奇怪的假设,因为它可能遇到未定义的行为。它必须假设变量值在范围内。编译器不能假设的是,读取之间的值是相同的 - 这是由某些DR解决的。