Significant decrease in performance after seemingly irrelevant changes

The answer is similar to Non-allocating loop over a set of structs - #6 by kristoffer.carlsson.

A loop is a chunk of code that is repeated. Here, letters will have a different type in each iteration so the code for the loop body that is compiled need to be general enough to handle this.

And like in the other answer, you can use Unrolled.jl to unroll the loop and thereby avoiding the constraint of a loop.

julia> @unroll function getSumX(letters::Tuple{Vararg{ABCs}})
           sum = 0.0
           @unroll for i = 1:length(letters)
               sum += letters[i].x
           end
           return sum
       end;

julia> @code_warntype getSumX((a1, a2, b))
Body::Float64
1 ─ %1  = π (0.0, Core.Compiler.Const(0.0, false))
│   %2  = π (1, Core.Compiler.Const(1, false))
│   %3  = (Base.getfield)(letters, %2, true)::A
│   %4  = (Base.getfield)(%3, :x)::Float64
│   %5  = (Base.add_float)(%1, %4)::Float64
│   %6  = π (2, Core.Compiler.Const(2, false))
│   %7  = (Base.getfield)(letters, %6, true)::A
│   %8  = (Base.getfield)(%7, :x)::Float64
│   %9  = (Base.add_float)(%5, %8)::Float64
│   %10 = π (3, Core.Compiler.Const(3, false))
│   %11 = (Base.getfield)(letters, %10, true)::B
│   %12 = (Base.getfield)(%11, :x)::Float64
│   %13 = (Base.add_float)(%9, %12)::Float64
└──       return %13