Is this function considered as re-entrant?

312 views Asked by At

I have a function with the following implementation:

void func (uint8 index, uint8 status)
{
  if (status == 1)
  {
    myArrayOfStructures[index].status = 1;
  }

  else if (status == 0)
  {
    myArrayOfStructures[index].status = 0;
  }

  else
  {
    /* Nothing */
  }
}

NB: myArrayOfStructures is a global variable on the file.

I thought that this function is re-entrant as long as the passing of its arguments is done through stack because of the following analysis:

The 2 arguments of the functions, on function call, are pushed onto stack. If the function is interrupted by itself from another OS task the argument are pushed onto stack a second time. Thus the 2 instances of the function are "independent" as each has its own set of arguments in stack.

Until, I have optimized this file for speed using a certain compiler option.

After optimization, I found that the passing of those arguments is done through registers (I know that the compiler has the right to do such thing (Register Allocation)). But this optimization has made this function non-re-entrant.

So, my question is

  • Is the above function really a re-entrant or not ?
  • If yes (re-entrant), how could the compiler make such an optimization which could revert its re-entrancy status ?

Please refer me to a reference with your answer.

Thanks a lot.

2

There are 2 answers

0
Sergey Kalinichenko On

Your function is re-entrant, regardless of the parameter passing mode the compiler chooses.

Although relying on global state for correct execution breaks re-entrancy, simply accessing global state does not necessarily make your function non re-entrant. In particular, if you never read global variables, as is the case in your function, your code is re-entrant, because its execution has no dependency on the global state.

As far as passing parameters through registers is concerned, a context switch saves the content of all registers as well, so the values are interrupt-safe regardless of the parameter passing mode.

0
Hadi Brais On

Probably not. I'll specify when it's reentrant and when it's not. But before doing that, you have to know that although there is only one set of physical registers per processor, the state of these registers is per thread. Each thread maintains its state and cannot fiddle with the register state of other threads. The OS ensures this.

In general, compiler optimizations, by definition, can never introduce bugs in correct code. However, an optimization may allow an existing bug to surface, but the bug is there in the code irrespective of whether the optimization is used or not. Therefore, whatever code you write, it should work correctly regardless of optimizations. There are exceptions to this, but they are not related to the question.

Now I'll answer your question. Suppose that the function has been called with some index and status of 1. Consider the following situation:

void func (uint8 index, uint8 status)
{
  if (status == 1)
  {
    // Interrupt occurs here.
    myArrayOfStructures[index].status = 1;
  }

  else if (status == 0)
  {
    myArrayOfStructures[index].status = 0;
  }

  else
  {
    /* Nothing */
  }
}

When the interrupt occurs and the same thread calls the same function with the same index and status of 0, it will set the value of the array element at that index to 0. When the first invocation resumes, it will write 1 to the same array element. I assume that you consider this to be incorrect behavior since the new status is lost. This shows that the function is not reentrant.

If the accesses to the array elements are not atomic, then the function is not reentrant even in the single-thread meaning of the word because that thread could be interrupted in the midst if updating or reading an array element.

Now let's consider two threads concurrently executing the function with the same index but with different statuses. In this case, A data race occurs. This means two things. First, the result is indeterministic. You don't know which status will be stored. If this is consistent with your correctness requirements, then it's fine. But probably, you want to store the latest status and so this indeterminism makes it non-reentrant. Second, which is a much bigger problem, is that a single data race makes your whole program to have undefined behavior according to the C standard. Which of course implies that the function is not reentrant.

In a symmetric multiprocessor system with no cache coherence or in a distributed memory computer system, the same array element can have multiple values and therefore you would be in the same situation where the most recent status is not known.

Compiler optimizations can make the function appear to be reentrant by reducing the probability of detecting a bug. For example, if the compiler can determine that the function is only called with status of either 0 or 1, then it can optimize the resulting assembly code to the following:

  1. Compare status and myArrayOfStructures[index].status for equality.

  2. Conditionally write status into myArrayOfStructures[index].status if not equal.

This is valid because global variables are initialized to zero and so every element in the array will be either 0 or 1. This code corresponds to two only instructions on x86 in which there are less cases that make the function non-reentrant.

In fact, compiler optimizations can even make the function reentrant. For example, if the compiler can determine that the function is only called with status of 0, then all the code in the function becomes dead-code making it effectively reentrant. That's why I said at the beginning "probably not."