GNAT, overflow checking and constant folding of loops

Why isn’t GNAT (v14 and older, checked on godbolt) able to constant fold the following loop, given that all the necessary information is known up front?

function Foo return Integer is
    Accm : Integer := 0;
begin
    for X in 1..100 loop
        Accm := @ + X*2;
    end loop;
    return Accm;
end Foo;

-O{1,2,3,s} all produces similar code and don’t eliminate the overflow checking like one would expect:

_ada_foo:
        mov     edx, 2    # ivtmp.9,
        xor     eax, eax  # <retval>
.L5:
        add     eax, edx  # tmp104, ivtmp.9
        jno     .L7       #,
        push    rax     #
        mov     esi, 5    #,
        mov     edi, OFFSET FLAT:.LC0     #,
        call    __gnat_rcheck_CE_Overflow_Check #
.L7:
        add     edx, 2    # ivtmp.9,
        cmp     edx, 202  # ivtmp.9,
        jne     .L5       #,
        ret  

Reduction expressions like [for X in 1..100 => X*2]'Reduce("+", 0) seem to be lowered to the above as well, no constant folding either.

Am I missing a simple compiler switch apart from pragma Suppress (Overflow_Check) and -gnatp that forces GNAT to not give up quite so easily? I don’t want to annotate every simple use case when the compiler should be able to detect these automatically at compile-time similar to the two example languages below.

Rust:

fn foo() -> i32 {
    let mut accm : i32 = 0;
    for x in 1..=100 {
        accm += x*2;
    }
    accm
}
fn foo() -> i32 {
    (1..=100).map(|x| x*2).sum()
}

D:

int foo() {
    int accm = 0;
    foreach (x; 1..101)
        accm += x*2;
    return accm;
}
int foo() => iota(1, 101).map!"a*2".sum;

Backed by GCC or LLVM, these compile down to mov eax, 10100 despite enabled overflow checking, no special flags apart from -O[s] needed.

-gnatp to suppress the run-time checks and you get the constant :+1:

Yes, I’m aware of that, but, as noted in the post, other languages don’t have to locally or globally disable overflow checking just to constant fold such a simple loop case.

Out of curiosity, does rust also fold that in debug mode? I know in release mode, Rust disables overflow checks on integers globally. I’m unfamiliar with D though.

You can definitely try talking with Adacore. It does seem like a case where GNAT should be able to detect and fold it at compile time. You can optionally try filing a bug report at GCC Bugzilla Main Page

I know in release mode, Rust disables overflow checks on integers globally

Damn, now I’m feeling rather dumb for not realizing that sooner. I just assumed they kept them in along the bounds checks and optimized them away as best as possible.

That prompted me to try a few more languages, and while some do runtime overflow checking, some silently overflow at compile-time given a large enough iteration value.

Well, in this case, I much rather stick to Ada’s strict approach.

Thanks and sorry for wasting everyone’s time. :sweat_smile:

Nononono. You did not waste anybody’s time (well, at least I can speak for the readers). It is these small details that set apart what looks alright and what actually is correct (I will let you decide which languages represent which :wink: )

It was something like this, but with some modular operations that I fell in love with Ada. Someone in Telegram pointed at a mundane piece of code running much much slower in Ada than other languages. When disassembled, the Ada code was double the size (we are talking 1/2 ASM instructions vs 5 or so other lines in the case of Ada). It turns out that other languages had some kind of operation (underflow?) be undefined and just did what they had to do regardless of what the underlying hardware did (in x86 this operation was also undefined, so good fucking luck with that). Ada/GNAT knew this and instead of creating performant code that relied on hardware-undefined-behaviour, it created the correct instructions to generate the expected result.

Best regards and keep on hacking and asking questions!
Fer

2 Likes

Interestingly they were turned off by default in atleast one version of Gnat until it was pointed out that Adas standard requires all compilers to enable them by default.

Well, that might explain why there hasn’t been all that much pressure in the past on eliminating overflow checks in rather simple cases.

It’s certainly interesting to look at the assembly and see instances where GNAT removes bounds checking based on analysis but leaves in the overflow code while seemingly being fully aware of the actual value ranges.

I would expect, priorities and maybe why take the risk for premature optimisations. The user can always prove there is no issue with Spark and turn the checks off for a particular performance sensitive section of code.

It is certainly known that Adas type information could leverage great performance optimisations potentially.