gpt4 book ai didi

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

转载 作者:行者123 更新时间:2023-11-30 19:06:47 25 4
gpt4 key购买 nike

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


如果已写入该项目,则读取将产生实际上已写入的值,并且
如果尚未写入该项目,则读取会将未指定的位模式转换为适当类型的值,而不会产生副作用。


从头开始找到解决方案的成本很高,但是验证所提出的解决方案的成本却很低。如果一个数组在所有情况下都具有解决方案,而在其他情况下则具有任意位模式,则读取该数组,测试其是否持有有效的解决方案,并仅在数组中的解决方案无效时才慢慢计算该解决方案,可能是有用的优化。

如果可以保证尝试读取类型如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;
}


只要可以将标识的存储作为该类型进行访问,就可以保证始终产生类型为 unsigned char的值,即使该存储恰好持有不确定值也是如此。但是,鉴于获得所需效果所需的机器代码通常应等效于返回 x,因此这种方法似乎很慢且笨拙。

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

附录

在使用部分写入的数组和结构执行I / O时,必须巩固值的能力,这种情况下,对于那些从未设置过的部件,什么位都不关心输出什么的情况下,就显得尤为重要。无论标准是否要求 fwrite可以用于部分扭曲的结构或数组,我都认为可以以这种方式使用的I / O例程(为未设置的部分写入任意值)应为比在这种情况下可能会跳脱的质量更高。

我主要关心的是防止最不可能在危险的组合中使用的优化,但是随着编译器变得越来越“聪明”,这种优化可能会发生。

出现类似问题:

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


是编译器可能组合了一个麻烦的优化,但在隔离方面是可以容忍的,而在隔离方面却是很好的,但与第一个结合可能是致命的:


如果传递了函数,则将 unsigned char的地址优化为例如。一个32位寄存器,上面的函数可能会盲目地返回该寄存器的内容,而不会将其裁剪到0-255的范围内。如果这是唯一的问题,则要求调用者手动裁剪此类函数的结果将很烦人,但可以生存。不幸...
由于上述函数将“始终”返回值0-255,因此编译器可能会省略任何“下游”代码,这些代码将尝试将该值屏蔽在该范围内,检查其值是否在范围之外,或者进行与该值无关的操作。值超出0-255范围。


某些I / O设备可能要求希望写入八位位组的代码执行16位或32位存储到I / O寄存器,并且可能要求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),编译器仍可以简化该表达式以产生上面给出的代码。

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

顺便说一句,当处理短数组时,初始化所有值将是廉价的并且通常是个好主意,而当使用大数组时,编译器将不太可能强加上述“优化”。在数组足够大而代价不菲的情况下,如果省略初始化,则会使程序的正确操作依赖于“希望”。

最佳答案

这不是答案,而是扩展的评论。

立即的解决方案是让编译器提供一个内置的(例如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()相对应的函数将重复 firstptrptr+first的字节。像 ptr+bytes-1一样,它将对数据的存储表示形式起作用,因此,即使 memmove()ptr包含陷阱表示形式,也不会实际触发陷阱。

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

例如,使用

    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 };


(如果将编译器定义为例如 ptr+first,其中 memsetall(data, size, count)是重复存储单元的大小,而 size存储单元总数(因此< cc>单元实际上是被复制的。)特别是,这使得使用非临时性存储来进行复制的简单实现(从初始存储单元读取);另一方面, count只能复制完整存储单元,与 count-1不同, memsetall()将使 memrepeat()中的第7个元素保持不变-即,在上面的示例中,它的产量为 memsetall(nums, 2 * sizeof nums[0], 3);。)

尽管您可以轻松实现 nums[]{ 7.0, 6.0, 7.0, 6.0, 7.0, 6.0, 1.0 },甚至可以针对特定的体系结构和编译器对其进行优化,但是编写可移植的优化版本却很困难。

特别是,使用 memrepeat()(或 memsetall())的基于循环的实现在通过例如GCC,因为编译器无法将函数调用模式合并到单个操作中。

大多数编译器通常将 memcpy()memmove()内联到目标和用例优化的内部版本中,并且对于这样的 memcpy()和/或 memmove()函数执行此操作将使其可移植。在x86-64上的Linux中,GCC内联已知大小的调用,但是将函数调用保留在仅在运行时知道大小的地方。

我确实尝试将其推向上游,并在各种邮件列表上进行了一些私人讨论和一些公开讨论。回应是亲切的,但很明确:没有办法将这些功能包含在编译器中,除非首先由某人对其进行了标准化,或者您激起了一个核心开发人员的兴趣,以至于他们想自己尝试一下。

因为C标准委员会只关心实现其公司赞助商的商业利益,所以将标准化的东西纳入ISO C的可能性几乎为零。(如果有的话,我们真的应该推动POSIX的基本功能,例如 memrepeat()memsetall()getline()首先加入;它们对我们可以教新的C程序员的代码产生更大的积极影响。)

这些都没有引起核心GCC开发人员的兴趣,因此在那时,我失去了将其推向上游的兴趣。

如果我的经验是典型的-并与少数人讨论,那么-OP和其他担心此类事情的人会更好地利用他们的时间来查找特定于编译器/体系结构的变通办法,而不是指出不足之处在标准中:标准已经丢失,那些人不在乎。

更好地将时间和精力花费在实际上可以完成的事情上,而不必与风车作斗争。

关于c - 从不确定值到未指定值的有效转换,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/47611338/

25 4 0
Copyright 2021 - 2024 cfsdn All Rights Reserved 蜀ICP备2022000587号
广告合作:1813099741@qq.com 6ren.com