当前位置: 首页 > 知识库问答 >
问题:

在C中忽略了易失性说明符

庾勇军
2023-03-14

我是C语言的新手,最近我偶然发现了一些关于变量易变性的信息。据我所知,这意味着对变量的读取或写入永远无法在不存在的情况下进行优化。

然而,当我声明一个不是1、2、4、8字节大的volatile变量时,出现了一种奇怪的情况:编译器(启用了C 11的gnu)似乎忽略了volatile说明符

#define expand1 a, a, a, a, a, a, a, a, a, a
#define expand2 // ten expand1 here, expand3 to expand5 follows
// expand5 is the equivalent of 1e+005 a, a, ....

struct threeBytes { char x, y, z; };
struct fourBytes { char w, x, y, z; };

int main()
{
   // requires ~1.5sec
   foo<int>();

   // doesn't take time
   foo<threeBytes>();

   // requires ~1.5sec
   foo<fourBytes>();
}

template<typename T>
void foo()
{
   volatile T a;

   // With my setup, the loop does take time and isn't optimized out
   clock_t start = clock();
   for(int i = 0; i < 100000; i++);
   clock_t end = clock();
   int interval = end - start;

   start = clock();
   for(int i = 0; i < 100000; i++) expand5;
   end = clock();

   cout << end - start - interval << endl;
}

他们的时间安排是

  • foo

我已经使用1到8个字节的不同变量(用户定义与否)对其进行了测试,只有1、2、4、8需要时间才能运行。这是我的设置中仅存在的错误,还是易失性是对编译器的请求,而不是绝对的?

PS:四字节版本的时间通常是其他版本的一半,也是一个混乱的来源


共有3个答案

吴哲
2023-03-14

volatile不会做你认为它会做的事。

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2006/n2016.html

如果您在我链接的页面上依赖Boehm提到的三个非常具体的用途之外的易失性,您将获得意想不到的结果。

谭玉泽
2023-03-14

这个问题比它第一次出现时有趣得多(对于“有趣”的一些定义)。看起来您发现了一个编译器错误(或故意不符合),但它并不是您所期望的。

根据标准,您的foo调用中的一个具有未定义的行为,而另外两个是格式不正确的。我将首先解释应该发生什么;Rest后可以找到相关的标准报价。出于我们的目的,我们可以只分析简单的表达式语句a,a,a 给定<代码>挥发性T a

a,a,a在这个表达式语句中是一个废弃的值表达式([stmt.expr]/p1)。表达式a,a,a的类型是右操作数的类型,即id表达式a,或volatile T;因为a是左值,所以表达式a,a,a([expr.comma]/p1)也是左值。因此,这个表达式是一个volatile限定类型的左值,它是一个“逗号表达式,其中右操作数是这些表达式之一”——特别是一个id表达式——因此[expr]/p11要求将左值到右值的转换应用于表达式a,a,a。类似地,在a,a,a内部,左表达式a,a也是一个废弃值表达式,在该表达式内部,左表达式a也是一个废弃值表达式;类似的逻辑表明,[expr]/p11要求对表达式a、a的结果和表达式a的结果(最左边的一个)应用左值到右值的转换。

如果T是类类型(三字节或四字节),应用左值到右值转换需要从易失性左值a([conv.lval]/p2)创建一个临时的by-copy初始化。然而,隐式声明的复制构造函数总是通过非易失性引用([class.copy]/p8)获取其参数;这样的引用不能绑定到易失性对象。因此,该计划的形式是错误的。

如果Tint,则将左值应用于右值转换将产生a中包含的值。但是,在代码中,a从未初始化;因此,此计算会产生一个不确定的值,并且根据[dcl.init]/p12,会导致未定义的行为。

下面是标准引语。都来自C 14:

[expr]/p11:

在某些情况下,表达式只会因其副作用而出现。这样的表达式称为废弃值表达式。表达式将被求值,其值将被丢弃。数组到指针(4.2)和函数到指针(4.3)的标准转换不适用。当且仅当表达式是volatile限定类型的glvalue且为以下之一时,才会应用左值到右值的转换(4.1):

  • (表达式),其中表达式是这些表达式之一,
  • id表达式(5.1.1),
  • [省略了几个不适用的项目符号],或
  • 逗号表达式(5.18),其中右操作数是这些表达式之一。

[注:使用重载运算符会导致函数调用;以上仅包括具有内置含义的运算符。如果左值是类类型,则必须有一个易失性副本构造函数来初始化作为左值到右值转换结果的临时值。-结束注]

[expr.comma]/p1:

由逗号分隔的一对表达式从左到右计算;左表达式是丢弃值表达式(第5条)[...]结果的类型和值是右操作数的类型和值;结果与其右操作数属于相同的值类别[...]。

[stmt.expr]/p1:

表达式语句的形式

expression-statement:
    expression_opt;

表达式是丢弃的值表达式(第5条)。

[conv.lval]/p1-2:

1非函数、非数组类型T的glvalue(3.10)可以转换为prvalue。如果T是一个不完整的类型,则需要进行此转换的程序格式不正确。如果T为非类别类型,则prvalue的类型为T的cv非限定版本。否则,PRT值的类型为T。

2[此处不适用的一些特殊规则]在所有其他情况下,转换结果根据以下规则确定:

  • [不适用的项目符号省略]
  • 否则,如果T具有类类型,则转换副本将从glvalue初始化类型为T的临时值,并且转换的结果是临时值的prvalue
  • [不适用的项目符号省略]
  • 否则,glvalue指示的对象中包含的值就是prvalue结果

[dcl.init]/p12:

如果没有为对象指定初始化程序,则该对象是默认初始化的。当获得具有自动或动态存储持续时间的对象的存储时,该对象具有不确定的值,如果没有为该对象执行初始化,则该对象保留不确定的值,直到该值被替换(5.17)。[...]如果评估产生不确定的值,则该行为是未定义的,除非在以下情况下:[与无符号窄字符类型相关的某些不适用的异常]

[class.copy]/p8:

X的隐式声明副本构造函数的形式为

X::X(const X&)

如果类类型M(或其数组)的每个潜在构造子对象都有一个复制构造函数,其第一个参数是const M类型

X::X(X&)

龙令
2023-03-14

结构版本可能会被优化,因为编译器意识到没有副作用(没有读取或写入变量a),而不管volatile。你基本上没有操作,a ,因此编译器可以随心所欲;它不会被迫展开循环或对其进行优化,因此volatile在这里并不重要。在ints的情况下,似乎没有优化,但这与volatile的用例是一致的:只有在循环中有可能“访问对象”(即读或写)时,才应该预期没有优化。然而,构成“访问对象”的内容是由实现定义的(尽管大多数情况下它遵循常识),请参见底部的编辑3。

这里的玩具示例:

#include <iostream>
#include <chrono>

int main()
{
    volatile int a = 0;

    const std::size_t N = 100000000;

    // side effects, never optimized
    auto start = std::chrono::steady_clock::now();
    for (std::size_t i = 0 ; i < N; ++i)
        ++a; // side effect (write)
    auto end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              <<  " ms" << std::endl;

    // no side effects, may or may not be optimized out
    start = std::chrono::steady_clock::now();
    for (std::size_t i = 0 ; i < N; ++i)
        a; // no side effect, this is a no-op
    end = std::chrono::steady_clock::now();
    std::cout << std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count()
              <<  " ms" << std::endl;
}

编辑

正如您在这个最小的示例中所看到的,no op实际上并没有针对标量类型进行优化。不过对于struct,它是经过优化的。在我链接的示例中,clang没有优化代码,而是使用-O3优化两个循环gcc也不会在没有优化的情况下优化循环,而是只在启用优化的情况下优化第一个循环。

编辑2

clang发出警告:警告:表达式结果未使用;赋给变量以强制可变负载[-Wunused volatile lvalue]。所以我最初的猜测是正确的,编译器可以优化出无操作,但它不是强制的。我不明白为什么它要为structs而不是标量类型这样做,但这是编译器的选择,而且符合标准。出于某种原因,它只在no op是结构时发出警告,而在它是标量类型时不发出警告。

另请注意,您没有“读/写”,您只有一个no-op,因此您不应该期望易失性带来任何东西。

编辑3

来自黄金书(C标准)

7.1.6.1/8 cv限定符[dcl.type.cv]

构成对具有可变限定类型的对象的访问的是实现定义的。。。

因此,何时优化循环取决于编译器。在大多数情况下,它遵循常识:当阅读或写入对象时。

 类似资料:
  • 问题内容: 在遍历多线程编程的许多资源时,通常会提到volatile说明符。显然,使用此关键字不是在C / C ++和Java(1.4版及更低版本)中至少实现多个线程之间同步的可靠方法。维基百科列出了此说明符的典型用法(未解释如何): 允许访问内存映射的设备 允许在setjmp和longjmp之间使用变量 允许在信号处理程序中使用变量 忙着等待 我可以开始在上面列出的用法中看到该说明符的角色,但是

  • 3.2.6.1.1. 忽略文件 如果有些文件不想与其他设备同步,则可以将其添加到忽略文件列表里,忽略文件存储在每个任务目录下的 .verysync/IgnoreList中,每行一个规则。 可以直接用普通文本编辑器修改该文件,也可以在界面目录选项里的忽略列表编辑器中进行修改。 忽略文件的修改仅在本机生效,并不会同步到其它设备上。 提示: 被忽略文件列表规则匹配到的文件将不扫描,不索引,不接收。 如果

  • 所有的中断函数都能正常工作,但是过程函数却让我很生气。 我会感激任何我没注意的把戏。

  • 我在配置Spring MessageSource以忽略我的系统区域设置时遇到问题。当我使用null locale参数调用getMessage时,我希望我的MessageSource选择默认属性文件messages.properties.相反,它选择messages_en.properties.当我将此属性文件的名称更改为messages_fr.properties然后选择默认属性文件。我的系统区域

  • 我最近将我的Win 8开发盒和CentOS 6部署环境升级到Tomcat 7.0.42。开始获取“信息:至少扫描了一个JAR的顶级域名,但没有包含顶级域名......”消息。根据其他SO问题,操作过程是在Tomcat的logging.properties中启用FINE日志,以找出哪些JAR是问题。 我已经试过了,但无济于事。无论我做什么,我都无法让Tomcat注意到我对${TOMCAT_HOME)

  • 问题内容: 我需要从第二个表中选择一些行,并将它们连接成逗号分隔的字符串。除一个问题外,查询效果很好-它始终选择所有行,并忽略LIMIT。 这是我的查询的一部分,该查询获取该字符串并忽略LIMIT: 完整查询: 问题答案: LIMIT子句限制最终结果集中的行数,而不是用于在GROUP_CONCAT中构造字符串的行数。由于您的查询在最终结果中仅返回一行,因此LIMIT无效。 您可以通过使用LIMIT