C 问题:如果我将变量的地址传递给修改它的函数,则可以保证该变量将被“重新加载”回来后?

发布于 2024-10-04 21:05:26 字数 2877 浏览 6 评论 0原文

我有很多过去与此类似的代码:

int num = 15;  

if(callback)  
  callback(&num);  /* this function may or may not change the value of num */  

if(num == 15)  /* I assumed num did not need to be volatile and is reloaded */  
  do_something();  
else  
  do_something_else();  

但是我现在有更复杂的结构和指向它们的指针:

struct example { int x,y,z; };  
struct example eg, eg2, *peg;   
int whatever;  
char fun;  

struct variables { struct example **ppeg; int *whatever; char *fun; };  
struct variables myvars;  

peg = ⪚  
myvars.ppeg = &peg;  
myvars.whatever = &whatever;  
myvars.fun = &fun;  
[...]  
peg->x = 15;  

if(callback)  
  callback(&myvars);  /* this function may or may not change the variables */  

if(peg->x == 15)  /* Can I assume that x is "reloaded" ? */  
  do_something();  
else  
  do_something_else();  

不管你相信与否,这仍然过于简化。现在显然我可以使用易失性,我假设我可以做这样的事情来强制重新加载:

if(*(volatile int *)&peg->x == 15)  

这能保证重新加载吗?换句话说,我以后是否能够简单地编写 if(peg->x) 知道一个易失性转换已经“重新加载”了变量?

问题是速度,因为我的函数可能会被连续调用数百万次。当然,它比上面要复杂得多。我想知道如果回调信号修改了变量是否有必要或更可取,以及是否有某种方法可以处理这个问题。我正在处理结构中的数十个变量,除非有必要,否则我不希望它们被“重新加载”。

另外,C99 标准是否对我的两个伪样本有深入的了解,例如保证在函数之后变量被“重新加载”(我不知道正确的词)。或者这个问题是编译器和优化级别特定的吗?我确实在 -O0 和 -O3 处使用 gcc 测试了其他一些类似的淡化样本,并且在任何一种情况下都没有看到差异(在所有情况下变量都有其正确的值)。

谢谢大家!



编辑 11/24/2010 1PM EST:为了解决我所说的“重新加载”的评论,我的意思是如果编译器在函数调用之前缓存变量(例如在寄存器或其他内存空间中),它仍然会访问函数调用后相同的缓存变量,而不是(可能)更新的变量。
编译器还可以循环提升以从循环中删除不更改的变量。就像如果我有 peg->x,而不是每次在循环中访问 peg->x,编译器是否可以确定即使回调被传递 &peg 也没有其他访问?如果我有这样的代码:

peg = ⪚  
while(1)  
{  
  if(!peg->x)  
    if(callback)  
      callback(peg);  

  if(!peg->x)  
    peg->x = 20;  

  if(peg == &eg)  
    peg = &eg2;  
  else  
    break;  
}  

那么编译器可以像这样优化它吗?例如:

while(1)  
{  
  if(!peg->x)  
  {  
    if(callback)  
      callback(peg);  

    peg->x = 20;  
  }  

  if(peg == &eg)  
    peg = &eg2;  
  else  
    break;  
}  

或者可以像这样优化它:

{  
  int someregister;  
  peg = ⪚  
  someregister = peg->x;  

  if(!someregister)  
    if(callback)  
      callback(peg);  

  if(!someregister)  
    peg->x = 20;  

  peg = &eg2;  
  someregister = peg->x;  

  if(!someregister)  
    if(callback)  
      callback(peg);  

  if(!someregister)  
    peg->x = 20;  
}  



编辑 11/24/2010 1:45PM EST:这是另一个例子。回调可以将指针更改为自身。

if(psomestruct->callback)  
{  
  psomestruct->callback(psomestruct); /* this callback could change the pointer to itself */  
  if(psomestruct->callback) /* will the compiler optimize this statement out? */  
    psomestruct->callback(psomestruct);  
}  

I have a lot of code that used to be similar to this:

int num = 15;  

if(callback)  
  callback(&num);  /* this function may or may not change the value of num */  

if(num == 15)  /* I assumed num did not need to be volatile and is reloaded */  
  do_something();  
else  
  do_something_else();  

However I now have more complicated structs and pointers to them:

struct example { int x,y,z; };  
struct example eg, eg2, *peg;   
int whatever;  
char fun;  

struct variables { struct example **ppeg; int *whatever; char *fun; };  
struct variables myvars;  

peg = ⪚  
myvars.ppeg = &peg;  
myvars.whatever = &whatever;  
myvars.fun = &fun;  
[...]  
peg->x = 15;  

if(callback)  
  callback(&myvars);  /* this function may or may not change the variables */  

if(peg->x == 15)  /* Can I assume that x is "reloaded" ? */  
  do_something();  
else  
  do_something_else();  

Believe it or not this is still overly simplified. Now obviously I could use volatile, I assume I could do something like this to force a reload:

if(*(volatile int *)&peg->x == 15)  

Does that guarantee a reload? In other words would I then later be able to write simply if(peg->x) knowing that the one volatile cast had already "reloaded" the variable?

The issue is speed as my function could be called continually, millions of times. It is of course much much more intricate than above. I wonder if it would be necessary or preferable to have the callback signal if it modifies the variables, and if there would be some way to handle that. I'm dealing with dozens of variables in the struct and I don't want them to be "reloaded" unless necessary.

Also, does the C99 standard have insight on either of my two pseudo samples, for example to guarantee that after a function that the variables are "reloaded" (I don't know the right word). Or is this issue compiler and optimization level specific? I did test some other similar watered down samples with gcc at -O0 and -O3 and I did not see a difference in either case (in all cases variables had their proper value).

Thanks everyone!

EDIT 11/24/2010 1PM EST: To address the comments on what I mean by "reloading" I meant if the compiler is caching the variable (in a register or other memory space for example) before the function call will it still access the same cached variable after the function call, rather than the (possibly) updated variable.
Also compilers can loop hoist to remove variables from the loop that do not change. Like if I have peg->x, rather than access peg->x each time in the loop could the compiler determine there are no other accesses even if the callback is passed &peg? If I have code like this:

peg = ⪚  
while(1)  
{  
  if(!peg->x)  
    if(callback)  
      callback(peg);  

  if(!peg->x)  
    peg->x = 20;  

  if(peg == &eg)  
    peg = &eg2;  
  else  
    break;  
}  

So could the compiler optimize it like this for example:

while(1)  
{  
  if(!peg->x)  
  {  
    if(callback)  
      callback(peg);  

    peg->x = 20;  
  }  

  if(peg == &eg)  
    peg = &eg2;  
  else  
    break;  
}  

or could it optimize it like this:

{  
  int someregister;  
  peg = ⪚  
  someregister = peg->x;  

  if(!someregister)  
    if(callback)  
      callback(peg);  

  if(!someregister)  
    peg->x = 20;  

  peg = &eg2;  
  someregister = peg->x;  

  if(!someregister)  
    if(callback)  
      callback(peg);  

  if(!someregister)  
    peg->x = 20;  
}  

EDIT 11/24/2010 1:45PM EST: Here's another example. The callback could change the pointer to itself.

if(psomestruct->callback)  
{  
  psomestruct->callback(psomestruct); /* this callback could change the pointer to itself */  
  if(psomestruct->callback) /* will the compiler optimize this statement out? */  
    psomestruct->callback(psomestruct);  
}  

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(2

好菇凉咱不稀罕他 2024-10-11 21:05:26

编译器不允许在可能修改变量的函数调用中“缓存”变量的值,只要您不调用任何未定义的行为。在这种情况下,未定义行为的特别相关实例是:

  • 修改声明为 const 的变量;并
  • 通过非 unsigned char 的不兼容类型的左值修改变量。

因此,例如,如果您有:

void modifier(int *a)
{
    *a += 10;
}

那么这将始终有效:

int i = 5;
modifier(&i);
if (i == 15)
    puts("OK");

但是,这可能不会

const int i = 5;
modifier((int *)&i);
if (i == 15)
    puts("OK");

这同样适用于您更复杂的示例。正如您的示例所示,确定函数是否可以修改给定变量所需的静态分析可能非常复杂。在这种情况下,编译器需要保持保守,这通常意味着在实践中,一旦获取了变量的地址,围绕该变量可以进行的优化就非常有限。


附录:

C 标准涵盖了这一点,但它并没有采取尝试列出所有可能的优化并声明是否允许的(徒劳的)方法。相反,C 标准表示(C99 中的第 5.1.2.3 节):

这里的语义描述
国际标准描述了
抽象机器的行为
哪些是优化问题
无关紧要。

换句话说,C 标准描述了一个以简单方式工作的抽象机,任何优化都必须确保任何在 C 抽象机上正确运行的程序也能在这些优化下正确运行。

修改对象(即使通过函数调用中的深层嵌套指针)是一个副作用。在 C 抽象机中,所有副作用均由下一个序列点完成(每个表达式的末尾都有一个序列点,例如在调用 callback()< /code> 在你的例子中)。 §5.1.2.3 中也对此进行了详细介绍。由于 C 抽象机中的对象一次只有一个值,因此修改完成后,对该对象的任何后续读取都必须读取新值。

因此,不需要任何 ignore_any_variables_previously_in_registers() - 在 C 抽象机下,没有在寄存器中缓存变量的概念(或者实际上根本没有“寄存器”) 。

如果您知道存储在嵌套结构深处的特定变量不会被函数调用修改,并且您希望允许在该函数调用中缓存它,只需创建一个局部变量来显式缓存它 - 完全按照您的 someregister 示例。如果合适的话,编译器将使用 someregister 的寄存器。

为了解决您的新示例,不允许编译器执行您建议的那些优化。如果它对分配给函数指针->callback的函数一无所知,它必须假设它可以更改任何变量,该变量的地址可能是可用指针别名的->callback()。实际上,这往往意味着只有地址未被获取的局部变量才能被认为是安全的。 C99 中引入了 restrict 关键字,以允许程序员就此类别名向优化器做出进一步的保证。

The compiler is not allowed to "cache" the value of a variable across function calls that may modify it, as long as you don't invoke any undefined behaviour. Particularly relevant instances of undefined behaviour in this context are:

  • modifying a variable that was declared const; and
  • modifying a variable through an lvalue of incompatible type that is not unsigned char.

So, for example, if you have:

void modifier(int *a)
{
    *a += 10;
}

then this will always work:

int i = 5;
modifier(&i);
if (i == 15)
    puts("OK");

However, this may not:

const int i = 5;
modifier((int *)&i);
if (i == 15)
    puts("OK");

The same applies to your more complicated examples. As your examples show, the static analysis required to determine if a function can or cannot modify a given variable can be quite complex. Compilers are required to be conservative in this case, which often means in practice that once you have taken the address of a variable, the optimisations that can be done around that variable are quite limited.


Addendum:

This is covered by the C standard, but it does not take the (futile) approach of trying to list every possible optimisation and declare whether it is allowed or disallowed. Instead, the C standard says (§5.1.2.3 in C99):

The semantic descriptions in this
International Standard describe the
behavior of an abstract machine in
which issues of optimization are
irrelevant.

In other words, the C standard describes an abstract machine that works in a straightforward manner, and any optimisations must ensure that any program that runs correctly on the C abstract machine also run correctly under those optimisations.

Modifying an object (even through a deeply nested pointer within a function call) is a side-effect. In the C abstract machine, all side-effects are complete by the next sequence point (there is a sequence point at the end of every expression, for example after the call to callback() in your example). This is also detailed in §5.1.2.3. Since objects in the C abstract machine have only one value at one time, after the modification is complete any subsequent reads of the object must read the new value.

Thus there is no need for any ignore_any_variables_previously_in_registers() - under the C abstract machine, there is no concept of caching variable in registers (or indeed, "registers" at all).

If you know that a particular variable stored deeply within your nested structs is not modified by the function call, and you want to allow it to be cached across that function call, simply create a local variable to explicitly cache it - exactly as per your example with someregister. The compiler will use a register for someregister if it is appropriate.

To address your new examples, the compiler is not allowed to perform those optimisations you suggest. If it knows nothing about the function assigned to the function pointer ->callback, it must assume that it can change any variable whose address is possibly aliased with a pointer available to ->callback(). In practice, this tended to mean that only local variables whose address had not been taken could be assumed to be safe. The restrict keyword was introduced in C99 to allow the programmer to make further guarantees to the optimiser about such aliasing.

攒眉千度 2024-10-11 21:05:26

不存在“重新加载”变量之类的事情。你可以省略易失性,你仍然会得到你想要的。

我认为您看到的是一个不存在的问题。

There is no such thing as "reloading" a variable. You can omit the volatile, you will still get what you want.

I think you are seeing a problem where none exists.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文