openlang

A slow gfortran loop that copied an array section on every call

A loop over the rows of a matrix, calling a subroutine with the row a(i, :) on every iteration. Right answer, embarrassing speed. The time goes into copying, not arithmetic.

subroutine scale(v, n)
  integer, intent(in) :: n
  real, intent(inout) :: v(n)
  v = 2.0 * v
end subroutine scale

! caller, a is m by n
do i = 1, m
  call scale(a(i, :), n)
end do

An explicit-shape dummy like v(n) assumes one contiguous run of memory. A row in Fortran isn't contiguous, neighbouring elements sit a whole column apart. So gfortran makes a temporary array on every call, copies the row in, and copies it back out after the call. Two copies per iteration, and nothing says so by default.

-Warray-temporaries points at the spot at compile time, and -fcheck=array-temps reports the temporary at run time. Both are in the debug build now. Whether to rewrite every such subroutine with v(:) or change the loop order is still open.

The conversation continues 2

Follow the reasoning
  1. Permalink

    Make the dummy assumed-shape, v(:). The caller then passes a descriptor with a stride, no copy.

    module ops
    contains
      subroutine scale(v)
        real, intent(inout) :: v(:)
        v = 2.0 * v
      end subroutine scale
    end module ops

    The call becomes call scale(a(i, :)) after use ops. It needs an explicit interface and the module provides one. Without it the call is invalid, and the compiler isn't required to notice.

    The copy goes. The stride stays. The subroutine still walks memory a column apart. If the loop order is free, loop over columns with a(:, j).

  2. Permalink

    Fun side effect here. Put contiguous on that v(:) dummy and the copy is back. The attribute promises the compiler that the dummy's elements sit next to each other, so a strided actual like a(i, :) has to be packed into a temporary before the call, same as with v(n). People add contiguous because it can help vectorization, and here it would quietly undo pgr's fix for exactly the calls that started this. I'd grep the sources for it before calling the loop fixed.

Other sides of the same question

  1. GOMAXPROCS in a container now follows the CPU limit

    Before Go 1.25, GOMAXPROCS defaulted to the logical CPU count of the machine. A container limited to two CPUs on a host with…

    Replies 8go
  2. try vs catch unreachable compares two different programs

    try and catch unreachable don't make the same promise, so timing one against the other measures two different programs. Both…

    Replies 4zig
  3. Does this queue need a cached count?

    The only thing the caller ever asks this queue is List.length items > 8, and it asks repeatedly. The queue is one immutable O…