You probably won't need Compare and Swap (CMPSWP) often, but when you do, you'll appreciate its high level of performance.
There are some types of data, an invoice number for instance, that have characteristics such that the "next" value...
• needs to be generated concurrently by several jobs,
• needs to be unique within the application,
• and is generally sequential in nature.
This type of data requirement historically has been satisfied with approaches such as using a data area (*DTAARA) to store and share the next available invoice number across the users of the application. While *DTAARAs work fine for this type of application, other options can provide this level of function at a much higher level of performance. One such option is the Compare and Swap (CMPSWP) Machine Interface (MI) instruction.
The CMPSWP instruction is shown below and documented here.
CMPSWP(
Op1 :address of a scalar (1,2,4,8) value
Op2 :address of a scalar (1,2,4,8) value
Op3 :scalar (1,2,4,8) value
Cntl :signed binary(4) literal value (optional)
) : signed binary(4)
This instruction requires three parameters, has one optional parameter, and returns a return value. We will be looking at the three required parameters and the return value today. The first three parameters can be defined as being 1, 2, 4, or 8 bytes in length, and all three must be of the same size. We will be using 4-byte, unsigned integer values today as this would accommodate an invoice number ranging from 0 to 4294967295. The return value is a 4-byte, signed integer value and will be discussed shortly.
When the CMPSWP instruction runs, the value of the first parameter (Op1) is compared to the value of the second parameter (Op2). If they are equal, the value of the third parameter (Op3) is copied (swapped) to the second parameter and the instruction returns a return value of 1. If they are not equal, the value of the second parameter is copied to the first parameter and the instruction returns a return value of 0. Further, the instruction assures that no other CMPSWP instruction on the system can access the value of the second parameter when parameters one and two do compare as equal.
Whew! Let's see what this means by way of an example.
Let's say we have a user space (*USRSPC) QGPL/HOLDNBR, and within the *USRSPC, HOLDNBR is stored as an unsigned integer value representing the last invoice number used. The *USRSPC could be created like this:
EDTF STMF('/qsys.lib/qgpl.lib/holdnbr.usrspc')
After using F3 to Save/Exit from the EDTF panel (and not typing anything into the panel), we will have a *USRSPC of minimum size initialized to x'00's. This is convenient as x'00's will be treated as if the last invoice number used was 0. With this *USRSPC created, here is the ILE CL program GETNBRCL that can be used to determine the next invoice number:
Pgm
Dcl Var(&NbrPtr) Type(*Ptr)
Dcl Var(&FndValue) Type(*UInt)
Dcl Var(&NewValue) Type(*UInt)
Dcl Var(&CurValue) Type(*UInt) Stg(*Based) +
BasPtr(&NbrPtr)
Dcl Var(&RtnCde) Type(*Int)
Dcl Var(&NewValChr) Type(*Char) Len(10)
Dcl Var(&SpcName) Type(*Char) Len(20) +
Value('HOLDNBR QGPL ')
(A) Call Pgm(QUSPTRUS) Parm(&SpcName &NbrPtr)
(B) ChgVar Var(&FndValue) Value(&CurValue)
DoUntil Cond(&RtnCde = 1)
ChgVar Var(&NewValue) Value(&FndValue + 1)
(C) CallPrc Prc('_CMPSWP') Parm((&FndValue) +
(&CurValue) (&NewValue *ByVal)) +
RtnVal(&RtnCde)
EndDo
ChgVar Var(&NewValChr) Value(&NewValue)
(D) SndPgmMsg Msg(&NewValChr) ToPgmQ(*Ext)
EndPgm
The variable &NbrPtr is a space pointer that is used to access the *USRSPC directly. This pointer is set by calling the Retrieve Pointer to User Space (QUSPTRUS) API at (A). The variables &FndValue, &NewValue, and &CurValue are 4-byte, unsigned integers representing the invoice number found in the *USRSPC, the next invoice number that could be used, and the current value in the *USRSPC, respectively. Note that the variable &CurValue is Based on the pointer variable &NbrPtr.
After setting &FndValue to the value of &CurValue at (B), GETNBRCL falls into a DoUntil loop where &NewValue is set by adding 1 to &FndValue. We then run the CMPSWP instruction. On the initial entry into the DoUntil loop, &FndValue will be 0 (from the newly created *USRSPC) and &NewValue set to 1. The CMPSWP instruction will compare &FndValue to &CurValue and, if they are equal (both are 0), then set &CurValue to 1 (the value of &NewValue) and return a return value of 1. This return value indicates that no other job on the system needed an invoice number between steps (B) and (C). The DoUntil loop is then exited with &NewValue holding the invoice number that should be used. This value is displayed at (D).
If the comparison between &FndValue and &CurValue is not equal (for example, two users needed invoice numbers between the running of (B) and (C), leaving &CurValue set to 2), then CMPSWP will set &FndValue to 2, leave &CurValue alone, and return a return value of 0. The DoUntil loop will then continue, setting &NewValue to 3 (a &FndValue of 2 plus 1) and using CMPSWP to determine if any other job has again modified the value found in the *USRSPC. This will continue until CMPSWP returns a return value of 1 (indicating success) or we attempt to generate a &NewValue value of 4294967296, which would exceed the capacity of a 4-byte unsigned integer, resulting in a MCH1210 error ('Receiver value too small to hold result') being sent by the system.
You might be thinking, "This looping doesn't occur when using data areas, so why would I take the CMPSWP approach? Won't it hurt my system's performance?" Those are reasonable questions, and I'll make two observations.
First, the looping occurs only when two or more users are trying to get an invoice number at the same time. With *DTAARAs, this same type of contention occurs, but you don't see it as your job is waiting, rather than looping, for access to the *DTAARA. This "waiting" takes more time than the DoUntil loop shown, so when contention occurs, CMPSWP will resolve that contention faster.
Second, in controlled testing comparing RPG *DTAARA *IN and *OUT operations with a CMPSWP solution generating 10000 invoice numbers, CMPSWP was consistently in the neighborhood of 100 times faster (5 ms vs. 565 ms on my 515). And please note that that is 100 times faster, not 100 percent faster!
This is the equivalent program in COBOL:
PROCESS NOMONOPRC NOSTDTRUNC.
IDENTIFICATION DIVISION.
PROGRAM-ID. GETNBRCBL.
ENVIRONMENT DIVISION.
CONFIGURATION SECTION.
SPECIAL-NAMES.
LINKAGE TYPE SYS FOR "_CMPSWP".
DATA DIVISION.
WORKING-STORAGE SECTION.
01 FndValue PIC 9(9) BINARY.
01 NewValue PIC 9(9) BINARY.
01 RtnCde PIC S9(9) BINARY.
01 SpcName PIC X(20)
VALUE "HOLDNBR QGPL ".
01 SpcPtr POINTER.
LINKAGE SECTION.
01 CurValue PIC 9(9) BINARY.
PROCEDURE DIVISION.
MAIN-LINE.
(A) CALL "QUSPTRUS" USING BY REFERENCE SpcName,
BY REFERENCE SpcPtr.
SET ADDRESS OF CurValue TO SpcPtr.
(B) MOVE CurValue TO FndValue.
PERFORM UNTIL RtnCde = 1
ADD 1 TO FndValue GIVING NewValue
(C) CALL "_CMPSWP" USING BY REFERENCE FndValue,
BY REFERENCE CurValue,
BY VALUE NewValue,
RETURNING RtnCde
END-PERFORM.
(D) DISPLAY NewValue.
STOP RUN.
And here's a slightly modified approach using RPG:
h dftactgrp(*no)
dGetNbr pr 10i 0 extproc('_CMPSWP')
d FndValue 10u 0
d CurValue 10u 0
d NewValue 10u 0 value
dRtvSpcPtr pr extpgm('QUSPTRUS')
d SpcName 20 const
d UsrSpcPtr *
dFndValue s 10u 0
dCurValue s 10u 0 based(NbrPtr)
dSpcName c 'HOLDNBR QGPL '
/free
(A) RtvSpcPtr(SpcName :NbrPtr);
(B) FndValue = CurValue;
(C) dow GetNbr(FndValue :CurValue :(FndValue + 1)) = 0;
enddo;
(D) dsply (FndValue + 1);
*inlr = *on;
return;
/end-free
The RPG approach is modified when compared to the CL and COBOL examples because of RPG's ability to use an expression as a parameter when calling CMPSWP. Rather than having a DoUntil loop where the program has a separate statement adding 1 to &FndValue and giving &NewValue, the RPG sample uses a DoWhile, which directly runs the CMPSWP instruction, exiting when the return value is 1. As part of running the instruction, the program uses an expression for the third parameter and eliminates the need for variable &NewValue.
Now, the question in your mind might be reversed: "Why wouldn't I use CMPSWP?" Again, a very reasonable question, and I'll point out two major reasons.
First, when CMPSWP is used in conjunction with a *USRSPC as we did here, it does not provide for any of the auditing and recovery features, such as journaling, that can be used with *DTAARAs. In fact, once an application has a pointer to the *USRSPC, the application can do almost anything to the contents of the *USRSPC and there will be no log of the changes, no journal entries to roll back inadvertent changes, and so on. To reset the invoice number to a prior value will mean restoring the *USRSPC from a backup or having an application program set the appropriate value. The relative importance of this aspect will depend on what type of data your application is working with. If we're talking invoice numbers, then this consideration may not be significant.
A second consideration is that this approach works only if all manipulation of the shared variable (&CurValue in our case) is done via CMPSWP. In most areas of the system, i5/OS manages data sharing on your behalf (for instance, record locking when updating and *DTAARA locking when updating), and updates are synchronized across jobs transparently. With CMPSWP, there is no i5/OS protection unless all applications use CMPSWP when modifying the value of the shared variable. A developer for instance could inadvertently code this:
ChgVar Var(&CurValue) Value(&NewValue)
This would cause an immediate change to &CurValue within the *USRSPC, potentially regressing &CurValue to an earlier value and causing duplication of invoice numbers. You will notice that the only change to &CurValue in the example program is done with CMPSWP. This is critical. Also, when displaying the value of the current invoice number, the program always uses a local variable such as &NewValue. This is because &CurValue can be changing dynamically because of work being done in other jobs. Keeping in mind the dynamic nature of &CurValue will take some getting used to for developers. *DTAARA values can also be corrupted due to errors in the application, but it's not done quite as easily!
I should point out a few items concerning the examples:
• If, when calling the CL program, you receive a run-time error, please make sure you have PTF SI28620 installed on your system. This PTF includes necessary CL compiler corrections related to pointer usage and will require that you recompile the sample CL program.
• Some of you might, upon reflection of the CMPSWP instruction, conclude that the statement shown at (B) in the previous examples is not needed. This is true! On the initial call to the CMPSWP instruction, you can pass in any value for &FndValue and, as long as &NewValue is set correctly, the samples will work properly. One time out of 4294967296, you will get "lucky" in that your arbitrary value for &FndValue happens to match &CurValue, and you will immediately exit the DoUntil loop. And one time out of 4294967296, you will receive a MCH1210 because you used a &FndValue of 4294967295 when trying to calculate &NewValue. But in the other 4294967294 cases, you will get a return value of 0 with the returned value of the parameter &FndValue set correctly for the next iteration of the Do loop. Personally, I recommend leaving the statement at (B) in as it provides better documentation of your intent, but it can be left out as long as you are willing to (most likely) loop through the Do logic one extra time.
• As COBOL doesn't support unsigned binary values, COBOL developers may want to use 8-byte binary values in order to support values greater than 2147483647. I used 4-byte values in the samples because CL does not support 8-byte binary variables.
The CMPSWP instruction is not one that you will use every day. But the power and speed of the instruction will make for an excellent tool in your development toolkit when the occasion does arise.
More CL Questions?
Wondering how to accomplish a function in CL? Send your CL-related questions to Bruce at
LATEST COMMENTS
MC Press Online