Recursive Program Performance

Article ID: 55351

Q: I'd like to write a recursive RPG program (not a procedure). Can this be done? What's the best method?

A: There are several ways to write a recursive program. In this article, I tell you what they are, how to write them, and show you which one performs the best.

The Problem

All ILE programs consist of a main procedure and one or more subprocedures. When an RPG program is called (using the CALL command or a similar tool) the main procedure is started. This main procedure can then run other subprocedures or programs as needed.

The main procedure in ILE RPG programs does not allow itself to be called more than once in the same activation group. Therefore, when you try to call an RPG program recursively, the main procedure detects that it's being called a second time in the same activation group, and returns an error message.

Method 1: ACTGRP(*NEW)

The obvious solution is to create a new activation group for each invocation of the program. This is very easy to do, just code your program with ACTGRP(*NEW). Since this will put each main procedure in it's own activation group, a recursive call will work.

The problem with specifying ACTGRP(*NEW) is performance. Creating a new activation group is a somewhat CPU intensive operation. If your program will call itself many times, performance will suffer.

Here's an RPG program named RECURSE1. This trivial program calls itself recursively 1,000 times, and checks how long the call took:

     H DFTACTGRP(*NO) ACTGRP(*NEW)

     D level           s              5p 0
     D msg             s             52a
     D Start           s               z
     D End             s               z

     C     *ENTRY        PLIST
     C                   PARM                    Level

     C                   if        Level = 0
     c                   eval      start = %timestamp()
     c                   endif

     C                   eval      Level = Level + 1

     C                   if        Level < 1000
     C                   CALL      'RECURSE1'
     C                   PARM                    Level
     c                   endif

     C                   eval      Level = Level - 1

     C                   if        Level = 0
     c                   eval      end = %timestamp()
     C     start         dsply
     C     end           dsply
     c                   endif

     C                   eval      *INLR = *ON

When the CALL statement runs, it calls itself. Since I've specified ACTGRP(*NEW) on the H-spec, the program will create a new activation group each time it's called. Therefore, this program will create 1,000 activation groups, and load a separate copy of the program into each one. At one point during execution, there will be 1,000 copies of this same program all loaded into memory, and all having their own copies of the variables.

The cost of creating a new activation group on every call is expensive. I'm running this program on a 600 CPW system, and this trivial program (which doesn't even access any files) takes 41 seconds to run. If you had to do this on every record in a one million record database, that would be more than 11 hours -- and that's before you factor in the time the databse spends loading records.

Method 2: Duplicating The Object

I've discussed this topic with people online, as well as people I've met at COMMON and other user group conferences over the years. I've heard several times from people that ACTGRP(*NEW) is too slow, and that they solved the problem another way: For each call, they made a duplicate of the program object with a different name, then called the duplicate. Because each program object has a different name, there's no conflict.

I have to admit that I thought this idea was a joke the first time I heard it. Duplicating the object just to avoid an activation group? That seems like jumping out of the frying pan and into the fire! Surely duplicating an object is slower than creating an activation group?!

To test this, I created RECURSE2, and here's the code for it:

     H DFTACTGRP(*NO)

     D level           s              5p 0
     D msg             s             52a
     D Start           s               z
     D End             s               z

     D QCMDEXC         PR                  ExtPgm('QCMDEXC')
     D   cmd                        200a   const options(*varsize)
     D   len                         15p 5 const
     D   igc                          3a   const options(*nopass)

     D cmd             s            200a   varying
     D newobj          s              9a

     C     *ENTRY        PLIST
     C                   PARM                    Level

     C                   if        Level = 0
     c                   eval      start = %timestamp()
     c                   endif

     C                   eval      Level = Level + 1

     C                   if        Level < 1000

     C                   eval      newobj = 'REC2' + %editc(Level:'X')

     C                   eval      cmd = 'CRTDUPOBJ OBJ(RECURSE2) +
     C                                              FROMLIB(*LIBL) +
     C                                              OBJTYPE(*PGM) +
     C                                              TOLIB(QTEMP) +
     C                                              NEWOBJ('+NewObj+')'
     C                   callp     QCMDEXC(cmd: %len(cmd))

     C                   CALL      newobj
     C                   PARM                    Level

     c                   endif

     C                   eval      Level = Level - 1

     C                   if        Level = 0
     c                   eval      end = %timestamp()
     C     start         dsply
     C     end           dsply
     c                   endif

     C                   eval      *INLR = *ON

I ran this program expecting it to be much slower than the ACTGRP(*NEW) version, however, I was disappointed. This program took 51 seconds to run, only 10 seconds longer than the ACTGRP(*NEW) version. It's a little slower, but not much! That startled me.

Then I decided to optimize this code a little bit. I added some additional statements that check to see if the program already exists before running the CRTDUPOBJ command.

     C                   if        Level < 1000

     C                   eval      newobj = 'REC2' + %editc(Level:'X')

     c                   eval      cmd = 'CHKOBJ OBJ(QTEMP/'+newObj+') +
     C                                           OBJTYPE(*PGM)'
     C                   callp(e)  QCMDEXC(cmd: %len(cmd))

     c                   if        %error
     C                   eval      cmd = 'CRTDUPOBJ OBJ(RECURSE2) +
     C                                              FROMLIB(*LIBL) +
     C                                              OBJTYPE(*PGM) +
     C                                              TOLIB(QTEMP) +
     C                                              NEWOBJ('+NewObj+')'
     C                   callp     QCMDEXC(cmd: %len(cmd))
     C                   endif

     C                   CALL      newobj
     C                   PARM                    Level

     c                   endif

The new version takes about the same time to run -- if I only run the recursive logic once, it's one second slower. However, if I run it a second time, it's much faster because it no longer has to duplicate the objects. The second call takes only 4 seconds, which blows the roof off of the ACTGRP(*NEW) program! Assuming that your recursive logic will be called multiple times, duplicating the object with this optimization definitely performs faster.

Clearly, I was wrong about the duplicate object option. It may be a bit more awkward to code, but it certainly outperforms ACTGRP(*NEW). But it's not the best performer, either!

Method 3: Eliminate the RPG Main Procedure

If the problem is that RPG's main procedure can't be called twice in the same activation, then why use it? ILE programs can all call each other's procedures, so why not use a CL main procedure instead of an RPG one? CL main procedures can be called recursively, so this should solve the problem.

The disadvantage of this technique is that I can't take advantage of the extra features that RPG's main procedure offers me -- namely, the RPG cycle. This isn't a big problem for me because frankly, I rarely use the cycle. However, if you're one of those folks who uses it, this technique might not be ideal for you.

To try this technique, I needed two modules. I use one CL module, named RECURSE3A, as the main procedure -- it does nothing but call an RPG subprocedure, and is very short.

PGM PARM(&LEVEL)
    DCL VAR(&LEVEL) TYPE(*DEC) LEN(5 0)
    CALLPRC PRC(RECURSE3B) PARM(&LEVEL)
ENDPGM

The CL module calls an RPG subprocedure named RECURSE3B, which I put into its own module, also named RECURSE3B. Here's the RPG code:

     H NOMAIN

     D RECURSE3B       PR
     D   Level                        5p 0

     P RECURSE3B       B                   export
     D RECURSE3B       PI
     D   Level                        5p 0

     D msg             s             52a
     D Start           s               z
     D End             s               z

     C                   if        Level = 0
     c                   eval      start = %timestamp()
     c                   endif

     C                   eval      Level = Level + 1

     C                   if        Level < 1000
     C                   CALL      'RECURSE3'
     C                   PARM                    Level
     c                   endif

     C                   eval      Level = Level - 1

     C                   if        Level = 0
     c                   eval      end = %timestamp()
     C     start         dsply
     C     end           dsply
     c                   endif
     P                 E

I used these two modules to create a program named RECURSE3. Here are the commands I used to compile it:

CRTCLMOD MODULE(RECURSE3A) SRCFILE(xxx/QCLSRC)
CRTRPGMOD MODULE(RECURSE3B) SRCFILE(xxx/QRPGLESRC)
CRTPGM PGM(RECURSE3) MODULE(RECURSE3A RECURSE3B) ACTGRP(QILE)

This version runs blindingly fast, about 1/4 second -- dramatically faster than any of the other alternatives. Since the code is not only faster, but also much simpler to write and maintain than the other examples, I have a hard time seeing why you'd want to do it any other way.

Of course, a recursive subprocedure is faster yet, and uses less memory. But when you must write a recursive program rather than a recursive procedure, the CL main procedure technique is by far the best performer.

ProVIP Sponsors

ProVIP Sponsors