This thread has been locked.

If you have a related question, please click the "Ask a related question" button in the top right corner. The newly created question will be automatically linked to this question.

Logical Negation Operator - Register Usage (-Ooff)

I am involved with safety critical software development to RTCA/DO-178C Level A.

Compiler:

    ti-cgt-arm_18.1.4.LTS

 

Options:

    -mv7R4

    --code_state=32

    -Ooff

    --opt_for_speed=0

    --c99

 

Consider the following C source code:

 

#define TRUE  1

#define FALSE 0

 

typedef unsigned char BOOL;

 

 

static BOOL Test_A (const BOOL ParamA);

 

 

int main(void) {

 

    (void) Test_A(TRUE);

 

    return (0);

}

 

 

static BOOL Test_A (const BOOL ParamA)

{

    BOOL Result = !ParamA;

 

    return (Result);

}

               

 

The disassembly for the line of source code which uses the logical negation operator is as follows:

 

 

21            BOOL Result = !ParamA;

00002894:   E5DD1000            ldrb       r1, [r13]

00002898:   E3A0C000            mov        r12, #0

0000289c:   E3510000            cmp        r1, #0

000028a0:   E3A00000            mov        r0, #0

000028a4:   0A000000            beq        $C$L1

000028a8:   E3A00001            mov        r0, #1

          $C$L1:

000028ac:   E3500000            cmp        r0, #0

000028b0:   1A000000            bne        $C$L2

000028b4:   E3A0C001            mov        r12, #1

          $C$L2:

000028b8:   E5CDC001            strb       r12, [r13, #1]

 

 

Even though optimisation is off (-Ooff), the implementation above seems unnecessarily complex, in particular the use of register r0.

 

Question:

Is there a legitimate reason as to why the implementation uses an 'additional' register (r0) and two labels rather than a single label implementation such as that detailed in the example below?

 

 

00002894:   E5DD0000            ldrb       r0, [r13]

00002898:   E3A0C001            mov        r12, #1

0000289c:   E3500000            cmp        r0, #0

000028a0:   0A000000            beq        $C$L1

000028a4:   E3A0C000            mov        r12, #0

          $C$L1:

000028a8:   E5CDC001            strb       r12, [r13, #1]

  • When optimization is disabled, there are no considerations for efficient code generation.  In addition to matching the actual source code, the compiler is going to generate function prolog and epilog blocks to control allocation/deallocation of the stack. The compiler is going to allocate local variables to the stack.  This may result in more branches than are necessary.  The compiler is not going to optimize the control flow or register usage.

  • Hi Alan, thanks for your response.

    I am very familiar with how function prolog and epilog blocks are generated to control allocation/deallocation and that the compiler is going to allocate local variables to the stack.

    I have included the full disassembly for my example function below.

     

              Test_A():

    0000288c:   E24DD008            sub        r13, r13, #8

    00002890:   E5CD0000            strb       r0, [r13]

    21            BOOL Result = !ParamA;

    00002894:   E5DD1000            ldrb       r1, [r13]

    00002898:   E3A0C000            mov        r12, #0

    0000289c:   E3510000            cmp        r1, #0

    000028a0:   E3A00000            mov        r0, #0

    000028a4:   0A000000            beq        $C$L1

    000028a8:   E3A00001            mov        r0, #1

              $C$L1:

    000028ac:   E3500000            cmp        r0, #0

    000028b0:   1A000000            bne        $C$L2

    000028b4:   E3A0C001            mov        r12, #1

              $C$L2:

    000028b8:   E5CDC001            strb       r12, [r13, #1]

    23            return (Result);

    000028bc:   E5DD0001            ldrb       r0, [r13, #1]

    24        }

    000028c0:   E28DD008            add        r13, r13, #8

    000028c4:   E12FFF1E            bx         lr

     

    The first two instructions constitute the prolog block:

    • reserve all the stack space necessary for execution
    • store the parameter from argument register r0 to the stack.

    As far as I can determine, after the second instruction has been executed register r0 is available to be used for another purpose.

     

    The final three instructions constitute the epilog block:

    • load the return register r0 with local variable ‘Result’ from the stack
    • restore the stack to the state before the function was entered
    • branch to address in the Link Register, the return address of the function call

     

    The single instruction prior to the epilog block stores the result of the logical negation to the local variable ‘Result’ on the stack.

     

    Given the above, I cannot see how either function prolog and epilog blocks or the allocation of local variables to the stack would be relevant to the implementation for the logical negation.

     

    I had hoped that you might be able to explain something required specifically for the implementation of logical negation that I have been unable to determine because, although I wouldn’t expect the optimal solution, surely even when optimisation is off (-Ooff) and ‘there are no considerations for efficient code generation’ register allocation would still be for a legitimate reason?

     

    I am surprised that if logical negation is achieved using an if statement then the compiler generates fewer instructions than when using the logical negation operator, see Test_B below.

     

    static BOOL Test_B (const BOOL ParamA)

    {

        BOOL Result = TRUE;

     

        if (ParamA != FALSE)

        {

            Result = FALSE;

        }

     

        return (Result);

    }

     

              Test_B():

    0000285c:   E24DD008            sub        r13, r13, #8

    00002860:   E5CD0000            strb       r0, [r13]

    32            BOOL Result = TRUE;

    00002864:   E3A0C001            mov        r12, #1

    00002868:   E5CDC001            strb       r12, [r13, #1]

    34            if (ParamA != FALSE)

    0000286c:   E5DDC000            ldrb       r12, [r13]

    00002870:   E35C0000            cmp        r12, #0

    00002874:   0A000001            beq        $C$L3

    36                Result = FALSE;

    00002878:   E3A0C000            mov        r12, #0

    0000287c:   E5CDC001            strb       r12, [r13, #1]

    39            return (Result);

              $C$L3:

    00002880:   E5DD0001            ldrb       r0, [r13, #1]

    40        }

    00002884:   E28DD008            add        r13, r13, #8

    00002888:   E12FFF1E            bx         lr

     

    I suspect that I will have to simply document the implementation used by the compiler for logical negation as suboptimal and state that the use of two registers and two labels is unnecessary.

  • I had hoped that you might be able to explain something required specifically for the implementation of logical negation that I have been unable to determine

    You are experiencing how compilers really work.  Early phases of the compiler generate code that is correct, but inefficient.  Sometimes very inefficient.  Later phases of the compiler, of which there are many, gradually change the code to something that is more efficient.  

    I suspect that I will have to simply document the implementation used by the compiler for logical negation as suboptimal and state that the use of two registers and two labels is unnecessary.

    That is probably the best way forward.

    Thanks and regards,

    -George