diff --git a/CHANGELOG.md b/CHANGELOG.md index f29d84df..7c32c89e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ Modifications by (in alphabetical order): * P. Vitt, University of Siegen, Germany * A. Voysey, UK Met Office +20/06/2022 PR #345 - add fparser2 performance benchmark in the scripts folder. + ## Release 0.0.16 (16/06/2022) ## 14/06/2022 PR #337 towards #312 (performance improvements). Removes some diff --git a/doc/developers_guide.rst b/doc/developers_guide.rst index f480c79a..7bb9897a 100644 --- a/doc/developers_guide.rst +++ b/doc/developers_guide.rst @@ -981,3 +981,15 @@ clear_symbol_table -- Removes all stored symbol tables. fake_symbol_table -- Creates a fake scoping region and associated symbol table. =================== ======================= =================================== + + +Performance Benchmark +--------------------- + +The fparser scripts folder contains a benchmarking script to assess the +performance of the parser by generating a synthetic Fortran file with +multiple subroutine and the associated subroutine calls. It can be executed +with the following command:: + + ./src/fparser/scripts/fparser2_bench.py + diff --git a/src/fparser/scripts/README b/src/fparser/scripts/README.md similarity index 87% rename from src/fparser/scripts/README rename to src/fparser/scripts/README.md index c9c4e79b..4bf5e911 100644 --- a/src/fparser/scripts/README +++ b/src/fparser/scripts/README.md @@ -1,6 +1,6 @@ This directory contains scripts that are potentially useful for -running and/or testing fparser. The scripts and their use is described -below +running and/or testing fparser. The scripts and their use are described +below. fparser2.py ----------- @@ -17,6 +17,12 @@ end program test PROGRAM test END PROGRAM test +fparser2_bench.py +----------------- + +Generates a synthetic Fortran source benchmark in memory and then +measures the time taken by fparser2 to parse it. + parse.py -------- diff --git a/src/fparser/scripts/fparser2_bench.py b/src/fparser/scripts/fparser2_bench.py new file mode 100755 index 00000000..6df464aa --- /dev/null +++ b/src/fparser/scripts/fparser2_bench.py @@ -0,0 +1,130 @@ +#!/usr/bin/env python +# Copyright (c) 2022 Science and Technology Facilities Council +# +# All rights reserved. +# +# Modifications made as part of the fparser project are distributed +# under the following license: +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +''' +Generates a large Fortran program in memory and then measures how long +it takes fparser2 to parse it. This is based on the benchmark suggested +by Ondřej Čertík via Ioannis Nikiteas. + +''' +from time import perf_counter + +from fparser.common.sourceinfo import FortranFormat +from fparser.common.readfortran import FortranStringReader +from fparser.two.parser import ParserFactory + + +def gen_sub(num: int): + ''' + Constructs a Fortran subroutine named g. + + :param num: the number of the subroutine (used to name it). + + :returns: Fortran subroutine. + :rtype: str + + ''' + sub = f"""subroutine g{num}(x) + integer, intent(inout) :: x + integer :: i + x = 0 + do i = {num}, {num+9} + x = x+i + end do +end subroutine + +""" + return sub + + +def create_bench(num_routines: int): + ''' + Creates the Fortran benchmark code. + + :param num_routines: the number of subroutines to create. + + :returns: benchmark Fortran code. + :rtype: str + + ''' + code = ["program bench3", + "implicit none", + "integer :: c", + "c = 0"] + for i in range(1, num_routines+1): + code.append(f"call g{i}(c)") + + code.append("print *, c") + code.append("contains") + + for i in range(1, num_routines+1): + code.append(gen_sub(i)) + code.append("end program") + + return "\n".join(code) + + +def runner(num_routines: int): + ''' + Entry point for running the benchmark. + + :param num_routines: the number of subroutines to create in the \ + Fortran benchmark. + + :raises ValueError: if num_routines < 1. + + ''' + if num_routines < 1: + raise ValueError(f"Number of routines to create must be a positive, " + f"non-zero integer but got: {num_routines}") + + print(f"Constructing benchmark code with {num_routines} subroutines...") + code = create_bench(num_routines) + reader = FortranStringReader(code) + # Ensure the reader uses free format. + reader.set_format(FortranFormat(True, True)) + + fparser = ParserFactory().create() + + print("Parsing benchmark code...") + tstart = perf_counter() + _ = fparser(reader) + tstop = perf_counter() + + print(f"Time taken for parse = {tstop - tstart:.2f}s") + + +if __name__ == "__main__": + runner(10000) # pragma: no cover diff --git a/src/fparser/scripts/tests/test_fparser2_bench.py b/src/fparser/scripts/tests/test_fparser2_bench.py new file mode 100644 index 00000000..4766395f --- /dev/null +++ b/src/fparser/scripts/tests/test_fparser2_bench.py @@ -0,0 +1,73 @@ +# Copyright (c) 2022 Science and Technology Facilities Council +# +# All rights reserved. +## +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are +# met: +# +# 1. Redistributions of source code must retain the above copyright +# notice, this list of conditions and the following disclaimer. +# +# 2. Redistributions in binary form must reproduce the above copyright +# notice, this list of conditions and the following disclaimer in the +# documentation and/or other materials provided with the distribution. +# +# 3. Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +# HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +'''Tests for the fparser2_bench script. + +''' + +import pytest +from fparser.scripts import fparser2_bench + + +def test_gen_sub(): + ''' Check the gen_sub() routine works as expected. ''' + code = fparser2_bench.gen_sub(3) + assert "subroutine g3(x)\n" in code + assert "do i = 3, 12\n" in code + assert "end subroutine\n" in code + + +def test_create_bench(): + ''' Check the create_bench() routine works as expected. ''' + code = fparser2_bench.create_bench(3) + assert "program bench3\n" in code + assert code.count("end subroutine") == 3 + for idx in range(1, 4): + assert f"subroutine g{idx}(x)" in code + assert "end program" in code + + +def test_runner_invalid_num_routines(): + ''' Test the checking on the value of the supplied num_routines + parameter. ''' + with pytest.raises(ValueError) as err: + fparser2_bench.runner(0) + assert ("Number of routines to create must be a positive, non-zero " + "integer but got: 0" in str(err.value)) + + +def test_runner(capsys): + ''' Check that normal usage gives the expected benchmark output. ''' + fparser2_bench.runner(3) + stdout, stderr = capsys.readouterr() + assert stderr == "" + assert "Constructing benchmark code with 3 subroutines" in stdout + assert "Time taken for parse =" in stdout diff --git a/src/fparser/two/tests/test_scripts.py b/src/fparser/scripts/tests/test_scripts.py similarity index 100% rename from src/fparser/two/tests/test_scripts.py rename to src/fparser/scripts/tests/test_scripts.py