MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Fortran循环结构深入解析

2021-07-172.5k 阅读

Fortran 循环结构基础

1. DO 循环基础

在 Fortran 中,DO 循环是一种基本的循环结构,用于重复执行一段代码。其基本语法如下:

DO i = start, stop, step
    ! 循环体代码
END DO

这里,i 是循环变量,start 是循环变量的初始值,stop 是循环变量的终止值,step 是每次循环后循环变量的增量。如果 step 省略,则默认值为 1。

例如,下面的代码计算 1 到 10 的整数之和:

program sum_example
    implicit none
    integer :: i, sum
    sum = 0
    DO i = 1, 10
        sum = sum + i
    END DO
    print *, '1 到 10 的和为:', sum
end program sum_example

在这个例子中,循环变量 i 从 1 开始,每次增加 1,直到 i 达到 10。在每次循环中,将 i 的值加到 sum 变量中。

2. 循环变量的作用域

循环变量 i 的作用域通常仅限于 DO 循环内部。这意味着在循环结束后,不能直接在循环外部引用循环变量 i。例如:

program scope_example
    implicit none
    integer :: sum
    sum = 0
    DO i = 1, 10
        sum = sum + i
    END DO
    ! 以下代码会报错,因为 i 在循环外不可用
    ! print *, '最后一个 i 的值为:', i
    print *, '1 到 10 的和为:', sum
end program scope_example

如果在循环外部需要使用循环变量的值,可以在循环结束前将其赋值给另一个变量。

3. 循环嵌套

在 Fortran 中,可以在一个循环内部嵌套另一个循环。这在处理多维数据结构(如矩阵)时非常有用。例如,下面的代码打印一个乘法表:

program multiplication_table
    implicit none
    integer :: i, j
    DO i = 1, 9
        DO j = 1, 9
            print '(I2, " * ", I2, " = ", I3)', i, j, i * j
        END DO
    END DO
end program multiplication_table

在这个例子中,外层循环控制乘法表的行数,内层循环控制每行的列数。每次外层循环迭代时,内层循环会完整执行一遍,从而打印出整个乘法表。

控制 DO 循环的执行

1. 提前终止循环(EXIT 语句)

有时候,可能需要在循环条件还未满足时提前终止循环。可以使用 EXIT 语句来实现这一点。例如,下面的代码在找到第一个能被 7 整除的数时终止循环:

program exit_example
    implicit none
    integer :: i
    DO i = 1, 100
        if (mod(i, 7) == 0) then
            print *, '找到第一个能被 7 整除的数:', i
            EXIT
        end if
    END DO
end program exit_example

在这个例子中,当 i 能被 7 整除时,EXIT 语句会立即终止循环,程序继续执行 END DO 之后的代码。

2. 跳过当前循环迭代(CYCLE 语句)

CYCLE 语句用于跳过当前循环迭代,直接进入下一次迭代。例如,下面的代码计算 1 到 10 中所有奇数的和:

program cycle_example
    implicit none
    integer :: i, sum
    sum = 0
    DO i = 1, 10
        if (mod(i, 2) == 0) then
            CYCLE
        end if
        sum = sum + i
    END DO
    print *, '1 到 10 中奇数的和为:', sum
end program cycle_example

在这个例子中,如果 i 是偶数,CYCLE 语句会跳过当前迭代,直接进入下一次循环,从而只对奇数进行求和。

与数组结合的循环

1. 遍历一维数组

循环经常用于遍历数组。例如,下面的代码计算一个一维数组中所有元素的和:

program array_sum_1d
    implicit none
    integer, dimension(5) :: arr = [1, 2, 3, 4, 5]
    integer :: i, sum
    sum = 0
    DO i = 1, size(arr)
        sum = sum + arr(i)
    END DO
    print *, '数组元素的和为:', sum
end program array_sum_1d

在这个例子中,size(arr) 函数返回数组 arr 的大小,循环通过索引 i 遍历数组的每个元素,并将其加到 sum 中。

2. 遍历多维数组

对于多维数组,需要使用嵌套循环。例如,下面的代码计算一个二维数组中所有元素的和:

program array_sum_2d
    implicit none
    integer, dimension(3, 3) :: arr = reshape([1, 2, 3, 4, 5, 6, 7, 8, 9], [3, 3])
    integer :: i, j, sum
    sum = 0
    DO i = 1, size(arr, 1)
        DO j = 1, size(arr, 2)
            sum = sum + arr(i, j)
        END DO
    END DO
    print *, '二维数组元素的和为:', sum
end program array_sum_2d

在这个例子中,size(arr, 1) 返回数组 arr 第一维的大小,size(arr, 2) 返回第二维的大小。外层循环控制第一维的索引 i,内层循环控制第二维的索引 j,从而遍历整个二维数组。

循环优化

1. 减少循环体内的计算

在编写循环时,应尽量减少循环体内的不必要计算。例如,下面的代码在每次循环中都计算数组的大小,这是不必要的:

program bad_loop
    implicit none
    integer, dimension(100) :: arr
    integer :: i, sum
    sum = 0
    DO i = 1, size(arr)
        sum = sum + arr(i)
    END DO
end program bad_loop

更好的做法是在循环外计算数组的大小,然后在循环中使用这个值:

program good_loop
    implicit none
    integer, dimension(100) :: arr
    integer :: i, sum, arr_size
    arr_size = size(arr)
    sum = 0
    DO i = 1, arr_size
        sum = sum + arr(i)
    END DO
end program good_loop

这样可以避免每次循环都重复计算数组的大小,提高程序的执行效率。

2. 向量化循环

现代 Fortran 编译器支持向量化优化。向量化循环可以将循环中的操作并行化,从而提高执行速度。例如,下面的代码计算两个数组对应元素的和:

program vectorized_loop
    implicit none
    integer, dimension(1000000) :: a, b, c
    integer :: i
    ! 初始化数组 a 和 b
    a = [(i, i = 1, 1000000)]
    b = [(i, i = 1, 1000000)]
    ! 传统循环
    DO i = 1, size(a)
        c(i) = a(i) + b(i)
    END DO
    ! 向量化循环(编译器会自动优化)
    c = a + b
end program vectorized_loop

在这个例子中,c = a + b 这种写法会被现代编译器识别并进行向量化优化,通常比传统的 DO 循环执行速度更快。为了充分利用向量化优化,数组的大小应该足够大,以抵消并行化带来的额外开销。

3. 缓存优化

缓存是计算机内存层次结构中的一个重要组成部分。在编写循环时,应尽量使数据访问模式与缓存的工作方式相匹配,以提高缓存命中率。例如,对于二维数组,按行访问通常比按列访问更有利于缓存命中。考虑下面的代码:

program cache_optimization
    implicit none
    integer, dimension(1000, 1000) :: arr
    integer :: i, j
    ! 按行访问
    DO i = 1, 1000
        DO j = 1, 1000
            arr(i, j) = i + j
        END DO
    END DO
    ! 按列访问
    DO j = 1, 1000
        DO i = 1, 1000
            arr(i, j) = i + j
        END DO
    END DO
end program cache_optimization

在 Fortran 中,数组在内存中是按列存储的。因此,按行访问(外层循环控制行索引 i,内层循环控制列索引 j)时,数据访问是连续的,更有利于缓存命中。而按列访问时,数据访问会跳跃,可能导致缓存命中率降低。

循环的性能分析

1. 使用系统时钟进行简单性能测量

在 Fortran 中,可以使用系统时钟函数来测量循环的执行时间。例如,下面的代码测量计算 1 到 1000000 整数之和的循环执行时间:

program performance_measurement
    implicit none
    integer :: i, sum
    real :: start_time, end_time
    sum = 0
    call system_clock(start_time)
    DO i = 1, 1000000
        sum = sum + i
    END DO
    call system_clock(end_time)
    print *, '循环执行时间为:', (end_time - start_time) / real(cycle_rate())
end program performance_measurement

在这个例子中,system_clock 函数获取当前系统时钟的计数值,cycle_rate 函数返回时钟的频率。通过计算开始和结束时钟计数值的差值,并除以时钟频率,可以得到循环的执行时间。

2. 使用性能分析工具

除了简单的系统时钟测量外,还可以使用专业的性能分析工具,如 Intel VTune、GNU gprof 等。这些工具可以提供更详细的性能信息,如函数调用次数、热点代码区域等。

以 gprof 为例,首先需要在编译时加上 -pg 选项:

gfortran -pg -o my_program my_program.f90

然后运行程序:

./my_program

程序运行结束后,会生成一个 gmon.out 文件。使用 gprof 工具分析这个文件:

gprof my_program gmon.out

gprof 会输出详细的性能报告,包括每个函数的调用次数、执行时间等信息,帮助开发者找到性能瓶颈并进行优化。

循环结构的高级应用

1. 生成数列

循环可以用于生成各种数列。例如,下面的代码生成斐波那契数列的前 20 项:

program fibonacci_sequence
    implicit none
    integer, dimension(20) :: fib
    integer :: i
    fib(1) = 0
    fib(2) = 1
    DO i = 3, 20
        fib(i) = fib(i - 1) + fib(i - 2)
    END DO
    DO i = 1, 20
        print '(I3.3, " : ", I10)', i, fib(i)
    END DO
end program fibonacci_sequence

在这个例子中,首先初始化斐波那契数列的前两项,然后通过循环计算后续的项。

2. 数值积分

循环在数值计算中经常用于实现数值积分。例如,下面的代码使用梯形积分法计算函数 $f(x) = x^2$ 在区间 $[0, 1]$ 上的积分:

program numerical_integration
    implicit none
    real :: a, b, n, h, integral
    integer :: i
    a = 0.0
    b = 1.0
    n = 1000000
    h = (b - a) / n
    integral = 0.0
    integral = integral + (func(a) + func(b)) / 2.0
    DO i = 1, n - 1
        integral = integral + func(a + i * h)
    END DO
    integral = integral * h
    print *, '积分结果为:', integral
contains
    real function func(x)
        real, intent(in) :: x
        func = x ** 2
    end function func
end program numerical_integration

在这个例子中,将积分区间 $[0, 1]$ 分成 $n$ 个小区间,使用梯形积分公式计算每个小区间的面积,并将它们累加起来得到积分结果。

3. 蒙特卡罗模拟

蒙特卡罗模拟是一种通过随机抽样来解决问题的方法,循环在其中起着关键作用。例如,下面的代码使用蒙特卡罗方法估计 $\pi$ 的值:

program monte_carlo_pi
    implicit none
    integer :: i, num_points
    real :: x, y, pi_estimate
    real, parameter :: num_points_real = 10000000
    num_points = int(num_points_real)
    integer :: inside_circle = 0
    DO i = 1, num_points
        call random_number(x)
        call random_number(y)
        if (x ** 2 + y ** 2 <= 1.0) then
            inside_circle = inside_circle + 1
        end if
    END DO
    pi_estimate = 4.0 * real(inside_circle) / real(num_points)
    print *, '估计的 π 值为:', pi_estimate
end program monte_carlo_pi

在这个例子中,随机生成 num_points 个点,这些点均匀分布在单位正方形 $[0, 1] \times [0, 1]$ 内。统计落在单位圆内的点的数量,通过比例关系估计 $\pi$ 的值。随着 num_points 的增加,估计值会越来越接近真实的 $\pi$ 值。

循环与并行计算

1. OpenMP 并行循环

OpenMP 是一种用于共享内存并行编程的 API。在 Fortran 中,可以使用 OpenMP 对循环进行并行化。首先需要在编译时加上支持 OpenMP 的选项,例如在 GCC 编译器中使用 -fopenmp

下面是一个使用 OpenMP 并行计算数组元素和的例子:

program openmp_example
    use omp_lib
    implicit none
    integer, dimension(1000000) :: arr
    integer :: i, sum
    sum = 0
    arr = [(i, i = 1, 1000000)]
    !$OMP PARALLEL DO REDUCTION(+ : sum)
    DO i = 1, size(arr)
        sum = sum + arr(i)
    END DO
    !$OMP END PARALLEL DO
    print *, '数组元素的和为:', sum
end program openmp_example

在这个例子中,!$OMP PARALLEL DO 指令表示接下来的 DO 循环将被并行化执行。REDUCTION(+ : sum) 子句用于指定 sum 变量在并行计算中的归约操作,即每个线程先独立计算部分和,最后将所有部分和累加起来得到最终结果。

2. MPI 并行循环(分布式内存)

MPI(Message Passing Interface)用于分布式内存并行编程。虽然 MPI 主要通过进程间通信来实现并行,但也可以结合循环进行数据并行计算。

下面是一个简单的 MPI 示例,计算数组元素的和。假设数组被均匀分配到各个 MPI 进程中:

program mpi_example
    use mpi
    implicit none
    integer :: ierr, rank, size
    integer, dimension(:), allocatable :: local_arr
    integer :: local_sum, global_sum
    integer :: local_size, total_size
    total_size = 1000000
    call MPI_Init(ierr)
    call MPI_Comm_rank(MPI_COMM_WORLD, rank, ierr)
    call MPI_Comm_size(MPI_COMM_WORLD, size, ierr)
    local_size = total_size / size
    allocate(local_arr(local_size))
    local_arr = [(rank * local_size + i, i = 1, local_size)]
    local_sum = 0
    DO i = 1, local_size
        local_sum = local_sum + local_arr(i)
    END DO
    call MPI_Reduce(local_sum, global_sum, 1, MPI_INTEGER, MPI_SUM, 0, MPI_COMM_WORLD, ierr)
    if (rank == 0) then
        print *, '数组元素的和为:', global_sum
    end if
    deallocate(local_arr)
    call MPI_Finalize(ierr)
end program mpi_example

在这个例子中,每个 MPI 进程计算自己所分配的数组部分的和,然后通过 MPI_Reduce 函数将所有进程的部分和累加到根进程(rank == 0),从而得到整个数组的和。

常见问题与解决方法

1. 循环变量未初始化

如果在使用循环变量时未进行初始化,可能会导致未定义行为。例如:

program uninitialized_loop_variable
    implicit none
    integer :: i
    DO i = start, 10 ! start 未定义
        print *, i
    END DO
end program uninitialized_loop_variable

解决方法是确保循环变量的初始值、终止值和步长都有明确的定义。

2. 无限循环

无限循环通常是由于循环条件设置不当导致的。例如:

program infinite_loop
    implicit none
    integer :: i
    i = 1
    DO
        print *, i
        i = i + 1
        if (i > 10) then
            EXIT
        end if
    END DO
    ! 以下代码可能会导致无限循环
    ! DO i = 1, 10, -1
    !     print *, i
    ! END DO
end program infinite_loop

在第二个 DO 循环中,由于步长为 -1,而初始值为 1,终止值为 10,循环变量 i 永远不会满足终止条件,从而导致无限循环。解决方法是仔细检查循环条件和步长,确保循环能够正常终止。

3. 数组越界

在循环中访问数组时,可能会发生数组越界错误。例如:

program array_out_of_bounds
    implicit none
    integer, dimension(5) :: arr = [1, 2, 3, 4, 5]
    integer :: i
    DO i = 1, 6 ! 循环会访问 arr(6),导致数组越界
        print *, arr(i)
    END DO
end program array_out_of_bounds

解决方法是在循环中确保数组索引在有效范围内,例如使用 size 函数来动态获取数组的大小。

Fortran 2003 及以后的循环特性

1. DO CONCURRENT 结构

Fortran 2003 引入了 DO CONCURRENT 结构,用于表达循环的并行性。例如,下面的代码并行计算两个数组对应元素的和:

program do_concurrent_example
    implicit none
    integer, dimension(1000000) :: a, b, c
    integer :: i
    a = [(i, i = 1, 1000000)]
    b = [(i, i = 1, 1000000)]
    DO CONCURRENT (i = 1:size(a))
        c(i) = a(i) + b(i)
    END DO
end program do_concurrent_example

DO CONCURRENT 结构告诉编译器循环中的迭代可以并行执行。与 OpenMP 不同,DO CONCURRENT 是 Fortran 语言标准的一部分,更注重表达并行意图,而 OpenMP 提供了更多的控制和优化选项。

2. 隐含形状数组与循环

Fortran 2003 及以后支持隐含形状数组。在循环中使用隐含形状数组可以提高代码的灵活性。例如:

program implied_shape_array_loop
    implicit none
    integer, allocatable :: arr(:, :)
    integer :: i, j
    allocate(arr(3, 3))
    arr = reshape([1, 2, 3, 4, 5, 6, 7, 8, 9], [3, 3])
    DO i = 1, size(arr, 1)
        DO j = 1, size(arr, 2)
            print '(I2)', arr(i, j)
        END DO
    END DO
    deallocate(arr)
end program implied_shape_array_loop

隐含形状数组可以在运行时根据需要分配内存,并且在循环中可以方便地使用 size 函数来遍历数组,而不需要事先知道数组的具体大小。

3. 过程指针与循环

过程指针允许在运行时动态选择要调用的函数。在循环中结合过程指针可以实现更灵活的计算。例如:

program procedure_pointer_loop
    implicit none
    real, external :: func1, func2
    real, pointer :: func_ptr => func1
    real :: x, result
    integer :: i
    DO i = 1, 10
        x = real(i)
        result = func_ptr(x)
        print *, 'x = ', x, ', result = ', result
        if (i == 5) then
            func_ptr => func2
        end if
    END DO
contains
    real function func1(x)
        real, intent(in) :: x
        func1 = x ** 2
    end function func1
    real function func2(x)
        real, intent(in) :: x
        func2 = x ** 3
    end function func2
end program procedure_pointer_loop

在这个例子中,开始时过程指针 func_ptr 指向 func1 函数,在循环执行到 i == 5 时,将 func_ptr 切换到 func2 函数,从而在循环中动态改变计算逻辑。

通过对 Fortran 循环结构的深入解析,从基础的 DO 循环到高级的并行计算应用,以及常见问题的解决方法和 Fortran 新特性的介绍,希望能帮助开发者更有效地使用循环结构,编写出高效、可靠的 Fortran 程序。无论是进行科学计算、工程模拟还是其他领域的应用,熟练掌握循环结构都是至关重要的。