【发布时间】:2024-01-11 18:00:02
【问题描述】:
在 C 中有时需要从部分写入的数组中读取可能写入的项,这样:
如果项目已被写入,则读取将产生实际写入的值,并且
如果项目尚未写入,读取会将未指定的位模式转换为适当类型的值,而不会产生副作用。
在各种算法中,从头开始寻找解决方案的成本很高,但验证提出的解决方案却很便宜。如果一个数组包含所有已找到的情况的解决方案,以及其他情况的任意位模式,则读取数组,测试它是否包含有效的解决方案,并且仅当数组中的解决方案无效时才慢慢计算解决方案,可能是一个有用的优化。
如果尝试读取类型为uint32_t 的非写入数组元素可以保证始终产生适当类型的值,那么这种方法将是简单直接的有效方法。即使该要求仅适用于unsigned char,它也可能仍然可行。不幸的是,编译器有时会表现得好像读取了一个不确定的值,即使是unsigned char 类型,也可能会产生与该类型的值不一致的东西。此外,缺陷报告中的讨论表明,涉及 Indeterminate 值的操作会产生 Indeterminate 结果,因此即使给定 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;
}
将保证在任何时候都可以生成类型为unsigned char 的值,只要标识的存储可以被访问为该类型,即使它碰巧持有不确定值。然而,这种方法似乎相当缓慢和笨拙,因为获得所需效果所需的机器代码通常应该等同于返回x。
是否有更好的方法可以保证标准始终产生unsigned char 范围内的值,即使源值是不确定的?
附录
固化值的能力是必要的,尤其是在使用部分写入的数组和结构执行 I/O 时,在没有人关心从未设置的部分输出哪些位的情况下。无论标准是否要求fwrite 可用于部分写入的结构或数组,我认为可以以这种方式使用的 I/O 例程(为未设置的部分写入任意值)是比那些在这种情况下可能会跳槽的质量更高。
我关心的主要是防范不太可能用于危险组合的优化,但随着编译器变得越来越“聪明”,这种优化仍然可能发生。
类似的问题:
unsigned char solidify_alt(unsigned char *p)
{ return *p; }
是编译器可能会将一个可能很麻烦但孤立地容忍的优化与一个孤立地很好但与第一个结合起来致命的优化结合起来:
如果函数被传递,
unsigned char的地址已被优化为例如一个 32 位寄存器,类似上面的函数可能会盲目地返回该寄存器的内容,而不会将其裁剪到 0-255 的范围内。要求调用者手动剪辑这些函数的结果会很烦人,但如果这是唯一的问题,那么它是可以生存的。可惜……
1234563与 0-255 范围之外的值无关的东西。
一些 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);
}
具有 send_string4("Ok"); 的缩进语义应该发送一个“O”、一个“k”、一个零字节和一个 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),编译器仍然可以简化它以生成上面给出的代码。
标准的作者没有要求编译器生成的自动变量初始化,因为在这种初始化在语义上是不必要的情况下,这会使代码变慢。我不认为他们打算让程序员在所有位模式都同样可接受的情况下必须手动初始化自动变量。
注意,顺便说一句,在处理短数组时,初始化所有值会很便宜,而且通常是个好主意,而当使用大数组时,编译器不太可能强加上述“优化”。但是,在数组足够大以至于成本很重要的情况下省略初始化将使程序的正确操作依赖于“希望”。
【问题讨论】:
-
出于好奇,有什么例子说明什么时候需要这样做?
-
我觉得这需要一个非常极端的情况才能值得在开始填写解决方案之前将数组归零。话虽如此,该标准使这一点变得多么困难和容易出错仍然令人非常沮丧。
-
您认为
solidify()与unsigned char solidify_alt(unsigned char *p) { unsigned char x = *p; return x; }相比有什么优势? -
委员会对DR 451 的回复说“[...] 对不确定值执行的任何操作都将具有不确定值。”因此,委员会的观点是,没有办法“确定”一个不确定的值,甚至是问题中提出的值。
-
没有任何实际价值的问题。在谈论 IO 的 POD 时更是如此。最后一个例子试图变得聪明,并表明不确定和未初始化是两个不同的东西。然而,类型转换神奇地切换到甚至在第一次使用
buff之前,而不是在分配OUTPUT_REG时在算术运算期间进行提升。作者很困惑为什么编译器会跟踪未初始化的变量来进行优化。高质量的代码完全不需要solidify,因为它首先不会调用未定义的行为。
标签: c undefined-behavior c99 c11