Performance and Compiler Options

This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production.

The reference material consulted while preparing these pages is listed in the section bibliography.

This page is written against ISO/IEC 1989:2023 (edition 3, January 2023; an upcoming 4th edition is in ballot), which defines the meaning of a program and none of its speed; against IBM Enterprise COBOL for z/OS 6.5 (PTFs through September 2026) on z/OS 3.2, whose compiler options are described in the Programming Guide; and against GnuCOBOL 3.2 (28 July 2023, the latest release), with which the programs on it were compiled with cobc -x -Wall and run on 2026-10-20. The timings below are from one throwaway Linux container and show relative sizes, not figures for any other machine. z/OS options are described from the Programming Guide and the Performance Tuning Guide and are labelled "z/OS; not run here".

A nightly batch run is judged by how long it takes and how much CPU it uses, and most of the gains come from a small number of decisions: choosing a numeric representation suited to the arithmetic, letting the compiler optimise, reading and writing files in large blocks, and not re-doing work inside loops. The page lists those decisions, says what each costs in safety, and explains how to measure before changing anything. The numeric formats are explained on USAGE and Numeric Representation, the options are listed on Compiling, Linking and Language Environment, the table techniques on Searching and Sorting Tables, and the run-time checks that cost time but find defects on Debugging and Troubleshooting.

Where the book is outdated: Taulli’s Modern Mainframe Development (2022) mentions performance only in passing. A separate volume on optimising COBOL by Vickler was not used for this page, and its claims (for example that COBOL has an XOR operator, or a "parallel OR") are not reproduced: they are not in the standard or in the Enterprise COBOL or GnuCOBOL manuals, so everything here comes from those manuals and from measurements shown.

Measure first

An optimisation that is not measured is a guess. Three levels of measurement are in use:

  • The job step. The job log (the IEF374I line and the step’s CPU time in the job statistics on z/OS; time on a Unix shell) gives elapsed and CPU time for a step. It is the number the operations team sees and the first one to compare before and after a change.

  • Language Environment storage and options reports. The run-time options RPTOPTS(ON) and RPTSTG(ON) make LE print the options in effect and a storage report at the end of the run, which tell whether the heap or stack are being enlarged repeatedly (an expensive operation) and what the right initial sizes would be. (z/OS; not run here)

  • SMF records. The system writes accounting records (type 30 for jobs and steps) with CPU and I/O counts; installations use them for charge-back and trend analysis. They are the source for "this step used 20 percent more CPU after the release".

Compare the same input before and after, run each three times, and change one thing at a time.

The cost of numeric USAGE

The same addition costs very different amounts depending on how the data is stored. The test program adds 3 to a nine-digit signed total twenty million times; only the USAGE of the total changes between runs.

      * BENCH - 20 million additions; only USAGE varies.
       IDENTIFICATION DIVISION.
       PROGRAM-ID. BENCH.
       DATA DIVISION.
       WORKING-STORAGE SECTION.
       01  WS-I               PIC 9(9) COMP-5.
       01  WS-TOTAL           PIC S9(9) DISPLAY VALUE 0.
       PROCEDURE DIVISION.
       MAIN-PARA.
           PERFORM VARYING WS-I FROM 1 BY 1 UNTIL WS-I > 20000000
               ADD 3 TO WS-TOTAL
           END-PERFORM
           DISPLAY "TOTAL " WS-TOTAL
           STOP RUN.

Compiled with cobc -x -Wall -O2 (no warnings) once for each of the USAGE values, with the time command around the run:

USAGE of WS-TOTAL Time (real) Note

DISPLAY (zoned decimal)

0.89 s

Every addition converts the digits to a number, adds and converts back.

COMP-3 (packed decimal)

0.05 s

Much cheaper than zoned decimal here; the arithmetic works on the packed digits directly.

COMP (binary), default truncation

0.31 s

The result is cut to nine decimal digits after every addition.

COMP (binary) with -fno-trunc

0.005 s

The binary item overflows only at the size of the binary field, as TRUNC(BIN) does on z/OS.

COMP-5 (native binary)

0.001 s

No truncation to the picture; the C compiler reduces the loop to little more than the count.

The ranking is the useful result, not the numbers. The COMP-5 loop is so short that the C compiler could optimise most of it away; a real program with I/O between the additions will not see ratios that large. The same run with -O0 for the DISPLAY case took 0.93 s and with -O2 -fno-trunc 0.90 s, so the optimisation level alone barely changed a program dominated by zoned-decimal conversions. For COMP and -fno-trunc the printed total has a different width (+0060000000 instead of +060000000) because the displayed value is the one allowed by the binary field, which is the visible side effect of turning truncation off.

On z/OS the same general ranking holds: arithmetic is fastest on binary items sized to fit a register (a PICTURE of nine digits or fewer in a fullword) and on packed decimal when the arithmetic uses decimal instructions, and it is slowest on zoned decimal that has to be unpacked; mixed usages in one expression add conversions. The practical rules are these:

  • Use COMP-3 (packed decimal) for money and for fields that are read from or written to files in that format; the arithmetic is exact and, with the right options, fast.

  • Use binary (COMP/BINARY) for subscripts, counters and indexes, and size them to a halfword (up to 4 digits), a fullword (up to 9) or a doubleword (up to 18).

  • Keep DISPLAY for what people read or what the file format demands, and move to a numeric usage for the computation.

  • Do not mix: a COMPUTE with a zoned operand, a packed one and a binary one converts at each step.

Options that trade safety for speed

Option What it changes Cost or risk

TRUNC(STD|OPT|BIN)

STD truncates a binary result to the number of digits in its PICTURE; BIN lets it use the full binary size; OPT lets the compiler choose.

STD is the safe, standard behaviour and the slowest for binary items; BIN and OPT can give results that differ from STD when a value exceeds the picture. GnuCOBOL’s -fno-trunc is the counterpart of BIN.

NUMPROC(NOPFD|PFD|MIG)

PFD tells the compiler that the signs in packed and zoned data are already the preferred ones, so it skips the repair of signs.

With bad data in the file PFD can produce wrong results instead of an error; use it only on data created by trusted programs.

SSRANGE

Generates code to check subscripts, indexes and reference modifications at run time.

Every checked reference costs a compare; use it for testing and where input is untrusted. CHECK suboptions at run time can turn the checks off without recompiling.

NUMCHECK

Checks numeric fields for valid contents when they are used.

The most expensive of the checks; use it during testing, to find the S0C7 before it happens.

INITCHECK

Warns about data items used before they are initialised.

A compile-time check; it costs nothing at run time.

OPTIMIZE(0|1|2)

0 is quick compilation with no optimisation, 1 and 2 optimise, 2 more, which may take longer to compile and may change the generated code in ways that matter to a debugger.

Compile time and debuggability, not correctness; production builds normally use OPTIMIZE(2) and test builds OPTIMIZE(0) with TEST.

ARCH(n) and TUNE(n)

ARCH is the oldest machine generation the load module must run on, and lets the compiler use instructions of that generation; TUNE orders instructions for the generation that will be running the program.

A module built with a high ARCH does not run on an older machine. Setting ARCH to the oldest machine in the installation is the usual rule.

FASTSRT

With SORT statements that use USING/GIVING or input/output procedures, lets DFSORT do the I/O of the sort files itself instead of calling back into the COBOL program.

Applies only when the files meet its rules; the sort work is faster, the code for the programs' own record processing is the same.

BLOCK0 and BLOCK CONTAINS 0

A file whose FD says BLOCK CONTAINS 0 RECORDS takes its block size from the data set definition (the JCL or the catalog), so the best block size can be chosen without recompiling. The BLOCK0 option makes the compiler treat files as if they had coded it.

Without it a file may be written with one record per block, which wastes space and I/O.

GnuCOBOL: -O2, -fno-trunc and friends

GnuCOBOL passes its optimisation choice to the C compiler: -O, -O2, -O3 and -Os enable optimisation and -O0 disables it. -fno-trunc allows numeric overflow of binary fields as in the table above, and -fbinary-truncate is the default behaviour. -fno-ec=name turns an individual run-time check off. A production build is normally cobc -x -O2 without -debug, and a test build is -debug with no optimisation; the per-statement run-time checks of -debug cost far more than the optimisation level saves.

Tables, loops and files

  • Indexes versus subscripts. An index (INDEXED BY) holds a displacement, a subscript holds an occurrence number that the compiler multiplies by the entry length at every reference. For tables searched in a loop, indexes (or binary subscripts) are faster. See Tables and OCCURS.

  • SEARCH ALL over SEARCH. A binary search on a table kept in key order examines about log2 n entries instead of n/2 on average. It requires the table to be in sequence on the KEY; sort it once at load time.

  • INITIALIZE in loops. INITIALIZE of a large group generates moves of every elementary item according to its class; inside a loop executed per record this may dominate the program. Initialise once, or move low-values to a group that is known to need only that, and keep the choice documented.

  • Reference modification and STRING. A STRING or INSPECT over a long field per record is costly; limit it to the used part of the field.

  • Files. Large blocks (use BLOCK CONTAINS 0 and a block size near half a track on disk), enough buffers (the BUFNO subparameter of the DD statement; the VSAM BUFND/BUFNI equivalents for VSAM files, see Indexed Files and VSAM), and sequential access instead of random access whenever the process can sort its input first.

  • CALL cost. Static calls are faster than dynamic calls; the trade-off is explained on Subprograms: CALL and Linkage.

Common mistakes

  • Optimising without measuring. The slow part of a nightly run is usually a file, a sort or a database call, not a COMPUTE.

  • Turning on NUMPROC(PFD) or TRUNC(BIN) for speed without testing the data. The run is faster and may be wrong; see the table above.

  • Shipping the test build. SSRANGE, NUMCHECK and TEST all slow the program.

  • Leaving BLOCK CONTAINS out. The data set then depends on the JCL default and the same program writes small blocks.

  • Believing a micro-benchmark. The table above ranks usages; it does not predict a real job.