Writing More Efficient Code
Follow these steps to write more efficient PeopleCode:
Declare all variables.
One of the conveniences of PeopleCode is that you do not have to declare your variables before you use them. The variable is assigned a type of ANY, taking on the type of the value it is assigned. However, if you use this feature, you lose type-checking at compile time, which can lead to problems at runtime.
When you validate or save PeopleCode, watch for auto-declared messages and consider adding declarations to your program.
Declare variable types specifically.
Most of the time, you know a variable's type, so you should declare the variable of that type when you begin.
For example, if you know that a particular variable is going to be an Integer value, declare it to be Integer in the first place. You can get much better runtime performance. It is particularly effective for loop control variables but, since an integer has limited range (up to 9 or 10 digits), you must use it judiciously.
In PeopleCode function calls, parameters are passed by reference; a reference to the value is passed instead of the value itself. If you are passing a reference to a complex data structure, such as a rowset object or an array, passing by reference saves significant processing.
Watch out for unexpected results, though. In the following code, the function Test changes the value of &Str after the function call.
Function Test(&Par as String) &Par = "Surprise"; end-function; Local String &Str = "Hello"; Test(&Str); /* now &Str has the value "surprise" */
Put Break statements in your Evaluate statements.
In an Evaluate statement, the When clauses continue to be evaluated until an End-evaluate or a Break statement is encountered.
If you have an Evaluate statement with a number of When clauses, and you only expect one of them to match, put a Break statement following the likely clause. Otherwise, all the subsequent When clauses are evaluated. Your program is still correct, but it is inefficient at runtime, particularly if you have a large number of When clauses, and the Evaluate statement is in a loop.
Govern your state.
One of the key features in PeopleSoft Pure Internet Architecture is that the application server is stateless. When required, the state of your session is bundled up and exchanged between the application server and the web server.
For example, on a user interaction, the whole state, including your PeopleCode state, has to be serialized to the web server. Then, once the interaction has completed, that state is deserialized in the application server so that your application can continue.
To improve efficiency:
Watch the size of PeopleCode objects that you create (strings, arrays, and so on) to make sure they are only as big as you need them to be.
For user interactions, you might be able to change the logic of your program to minimize the state.
For example if you are building up a large string (a couple of megabytes) and then performing a user interaction, you might be able to change your program logic to build the string after the interaction.
For secondary pages that are infrequently accessed but retrieve lots of data, consider setting No Auto Select in the Application Designer for the grids and scroll areas on the secondary page, to prevent loading the data the secondary page when the page buffers are initially built.
Then add the necessary Select method to the Activate event for the secondary page to load the data into the grid or scroll area.
Isolate common expressions.
The PeopleCode compiler is not an optimizing compiler, unlike some current compilers for languages such as C++. For example, the PeopleCode compiler does not do common subexpression analysis. So, sometimes, if you have a complicated bit of PeopleCode that is used often, you can isolate the common expression yourself. This isolation can make your code look cleaner and make your code faster, especially if it is in a loop.
In this example, notice how the common subexpression is broken out:
/*---- For this customer, setup time on B is influenced by *---- the machine flavors of A. */ &r_machine = &rs(&idB.GetRecord(Record.MACHINE_INFO); If (&typeA = "F") And (&typeB == "U") Then &r_machine.SETUP_TIME.Value = 50; Else &r_machine.SETUP_TIME.Value = 10; End-If;
The compiler has to evaluate each occurrence of the expression, even though it would only execute it once.
Here is another example. Notice that once &RS and &StartDate are created, they can be used repeatedly in the loop, saving significant processing time.
&RS = GetRowset(); &StartDate = GetField(PSU_CRS_SESSN.START_DATE).Value; For &I = 1 To &RS.ActiveRowCount &RecStuEnroll = &RS.GetRow(&I).PSU_STU_ENROLL; &Course = &RecStuEnroll.COURSE; &Status = &RecStuEnroll.ENROLL_STATUS; &PreReqStart = &RS.GetRow(&I).PSU_CRS_SESSN.START_DATE.Value; If &Course.Value = "1002" And (&Status.Value = "ENR" Or &Status.Value = "CMP") Then If &PreReqStart < &StartDate Then &Completed = True; Break; End-If; End-If; End-For;
Avoid implicit conversions.
The most common implicit conversion is from a character string to a number and vice versa. You might not be able to do anything about this, but—by being aware of it—you might be able to spot opportunities to improve performance.
In the following example, two character strings are converted into numeric values before the difference is taken. If this code were in a loop and one of the values did not change, performance would improve significantly by doing the conversion once, as the second statement illustrates.
&Diff = &R1.QE_EMPLID.Value - &R2.QE_EMPID.Value; &Original = &R1.QE_EMPLID.Value; . . . &Diff = &Original - &R2.QE_EMPID.Value;
Choose the right SQL style.
In certain cases, use SQLExec, as it only returns a single row. In other cases, you could benefit greatly by using a SQL object instead, especially if you can plan to execute a statement more than once with different bind parameters. The performance gain comes from compiling the statement once and executing it many times.
For instance, code that uses SQLExec might look like this:
While (some condition) . . .set up &Rec SQLExec("%Insert(:1)", &rec); /* this does a separate tools parse of the sql and db compile of the statement and execute each time */ End-while;
The following code rewrites the previous example to use the new SQL object:
Local SQL &SQL = CreateSQL("%Insert(:1)"); While (some condition) . . .Setup &Rec &Sql.Execute(&Rec); /* saves the tools parse and db compile on the SQL statement and the db setup for the statement */ end-while;
SQL objects also have the ReuseCursor property, which can be used for further performance gains.
Tighten up loops.
Examine loops to see if code can be placed outside the loop.
For example, if you are working with file objects and your file layout does not change, there is no reason to set the file layout every time you go through the loop reading lines from the file. Set the file layout once, outside the loop.
Set objects to NULL when they will no longer be accessed.
Once you are finished with an object reference, especially one with a global or component scope, assign it to NULL to get rid of the object. This setting allows the runtime environment to clean up unused objects, reducing the size of your PeopleCode state.
Improve your application classes
Simple properties (without get/set) are much more efficient than method calls. Be clear in your design about what needs to be simple properties, properties with get/set, and methods. Never make something a method that really should be a property.
Analyze your use of properties implemented with get/set. While PeopleCode properties are in a sense first class properties with more flexibility in that you can run PeopleCode to actually get and set their values, make sure you actually need get and set methods. If all you have is a normal property which is more of an instance variable then avoid get/set methods. In the following example (without the strikethrough!) by having get/set for the property SomeString you have made it much more inefficient to get/set that property since every property reference has to run some PeopleCode. Often, this inneficiency can creep in when properties are designed to be flexible at the beginning and never subsequently analyzed for whether getters/setters were really needed after all.
class Test ... property String SomeString get set; end-class;get SomeString return &SomeString; end-get; set SomeString &SomeString = &NewValue; end-set;
These examples demonstrate more efficiently written code:
Beware of the rowset Fill method. (Or, "What not to do in a Application Engine PeopleCode step.")
Sometimes you need to examine the algorithm you are using. The following example is a PeopleCode program that adopts this approach: read all the data into a rowset, process it row by row, and then update as necessary. One of the reasons this is a bad approach is because you lose the general advantage of set-based programming that you get with Application Engine programs.
Local Rowset &RS; Local Record &REC; Local SQL &SQL_UPDATE; &REC_NAME1 = "Record." | SOME_AET.SOME_TMP; &RS = CreateRowset(@(&REC_NAME1)); &LINE_NO = 1; &NUM_ROWS = &RS.Fill("WHERE PROCESS_INSTANCE = :1 AND BUSINESS_UNIT = :2 AND TRANSACTION_GROUP = :3 AND ADJUST_TYPE = :4 ", SOME_AET.PROCESS_INSTANCE, SOME_AET.BUSINESS_UNIT, SOME_AET.TRANSACTION_GROUP, SOME_AET.ADJUST_TYPE); For &I = 1 To &NUM_ROWS &REC = &RS(&I).GetRecord(@(&REC_NAME1)); &REC.SOME_FIELD.Value = &LINE_NO; &REC.Update(); &LINE_NO = &LINE_NO + 2; End-For;
This code has the following problems:
You might run out of memory in the Fill method if the Select gathers a large amount of data.
The Fill is selecting all the columns in the table when all that is being updated is one column.
You can change this code to read in the data one row at a time using a SQL object or using a similar algorithm, but chunking the rowsets into a manageable size through the use of an appropriate Where clause.
The following are some approximate numbers you can use to see how large a rowset can grow. The overhead for a field buffer (independent of any field data) is approximately 88 bytes. The overhead for a record buffer is approximately 44 bytes. The overhead for a row is approximately 26 bytes. So a rowset with just one record (row) the general approximate formula is as follows:
memory_amount = nrows * (row overhead + nrecords * ( rec overhead + nfields * ( field overhead) + average cumulative fielddata for all fields))
The following are some code examples to show isolating common expressions.
In this example, a simple evaluation goes from happening three times to just once–
&RS_Level2(&I).PSU_TASK_EFFORT. In addition, the rewritten code is easier to read.
Example of code before being rewritten:
Local Rowset &RS_Level2; Local Boolean &TrueOrFalse = (PSU_TASK_RSRC.COMPLETED_FLAG.Value = "N"); For &I = 1 To &RS_Level2.ActiveRowCount &RS_Level2(&I).PSU_TASK_EFFORT.EFFORT_DT.Enabled = &TrueOrFalse; &RS_Level2(&I).PSU_TASK_EFFORT.EFFORT_AMT.Enabled = &TrueOrFalse; &RS_Level2(&I).PSU_TASK_EFFORT.CHARGE_BACK.Enabled = &TrueOrFalse; End-For;
Example of code after being rewritten:
Local Boolean &TrueOrFalse = (PSU_TASK_RSRC.COMPLETED_FLAG.Value = "N"); For &I = 1 To &RS_Level2.ActiveRowCount Local Record &TaskEffort = &RS_Level2(&I).PSU_TASK_EFFORT; &TaskEffort.EFFORT_DT.Enabled = &TrueOrFalse; &TaskEffort.EFFORT_AMT.Enabled = &TrueOrFalse; &TaskEffort.CHARGE_BACK.Enabled = &TrueOrFalse; End-For;
In the next example, the following improvements are made to the code:
Shorthand is used: &ThisRs(&J) instead of &ThisRs.GetRow(&J).
Eliminated all the autodeclared messages by declaring all the local variables. This action can improve your logic and possibly give you better performance.
Notice the integer declaration. If you know your variables will fit in an integer (or a float), then declare them that way. Runtime performance for Integers can be better than for variables declared as Number.
Fewer evaluation expressions.
Example of code before being rewritten:
Local Row &CurrentRow; &TrueOrFalse = (GetField().Value = "N"); &CurrentRow = GetRow(); For &I = 1 To &CurrentRow.ChildCount For &J = 1 To &CurrentRow.GetRowset(&I).ActiveRowCount For &K = 1 To &CurrentRow.GetRowset(&I).GetRow(&J).RecordCount For &L = 1 To &CurrentRow.GetRowset(&I).GetRow(&J).GetRecord(&K).FieldCount &CurrentRow.GetRowset(&I).GetRow(&J).GetRecord(&K).GetField(&L).Enabled = &TrueOrFalse; End-For; End-For; End-For; End-For;
Example of code after being rewritten:
Local Row &CurrentRow; Local integer &I, &J, &K, &L; Local boolean &TrueOrFalse = (GetField().Value = "N"); &CurrentRow = GetRow(); For &i = 1 To &CurrentRow.ChildCount /* No specific RowSet, Record, or Field is mentioned! */ Local Rowset &ThisRs = &CurrentRow.GetRowset(&i); For &J = 1 To &ThisRs.ActiveRowCount Local Row &ThisRow = &ThisRs(&J); For &K = 1 To &ThisRow.RecordCount Local Record &ThisRec = &ThisRow.GetRecord(&K); For &L = 1 To &ThisRec.FieldCount &ThisRec.GetField(&L).Enabled = &TrueOrFalse; End-For; End-For; End-For; End-For;
Concatenating a large number of strings into a large string. Sometimes you need to do this.
The simplest approach is to do something like:
&NewString = &NewString | &NewPiece;
In itself this is not a bad approach but you can do this much more efficiently using an application class below.
class StringBuffer method StringBuffer(&InitialValue As string); method Append(&New As string) returns StringBuffer; // allows &X.Append("this").Append("that").Append("and this") method Reset(); property string Value get set; property integer Length readonly; property integer MaxLength; private instance array of string &Pieces; end-class; method StringBuffer /+ &InitialValue as String, +/ &Pieces = CreateArray(&InitialValue); &MaxLength = 2147483647; // default maximum size &Length = Len(&InitialValue); end-method; method Reset &Pieces.Len = 0; &Length = 0; end-method; method Append /+ &New as String +/ Local integer &TempLength = &Length + Len(&New); If &Length > &MaxLength Then throw CreateException(0, 0, "Maximum size of StringBuffer exceeded(" | &MaxLength | ")"); End-If; &Length = &TempLength; &Pieces.Push(&New); return %This; end-method; get Value /+ Returns String +/ Local string &Temp = &Pieces.Join("", "", "", &Length); /* collapse array now */ &Pieces.Len = 1; &Pieces = &Temp; /* start out with this combo string */ Return &Temp; end-get; set Value /+ &NewValue as String +/ /* Ditch our current value */ &Pieces.Len = 1; &Pieces = &NewValue; /* start out with this string */ &Length = Len(&NewValue); end-set;
Use this code as follows:
Local StringBuffer &S = create StringBuffer(""); .... &S.Append(&line); /* to get the value of string simply use &S.Value */