There are several situations which always trigger me in wanting to know which implementation yields the best performance. In this article I’d like to share three of these implementation-options, including some high-over performance-observations, which for me, now easily rule out some options for future implementations.

TLDR; The most optimal implementations for loops and Maps are:
for( Integer i = 0, j = objList.size(); i < j; i++ ){ Object o = objList[ i ]; };
mapVar.put( key, value ); without a containsKey-condition, as long as number of duplicates is < 50%;
– For a nested Map, always get() the first level, compare for null, instead of containsKey().

Put, within a containsKey() or assign duplicate

When looping over a list of records and assigning it to a Map, I was always curious whether preventing a put, but spending time on a containsKey() on each iteration would be more or less performing.

Of course, this scenario is only relevant when value would be the same for all list-elements; thus when it doesn’t make a difference whether you assign the first or the last value for the same key.

Setup

To test this, I’ve set up a very simple Anonymous Apex:

  • A list of 600k+ Integers;
  • Varying the number of duplicate Integers to have more of less impact of the put-prevention;
  • Logic to populate two Maps
    • 1. Prevent put-assignments when the key already exists, by always first checking containsKey();
    • 2. Always put all values, potentially overwriting and performing same action multiple times.

Results

The results are rather logical when interpreting:

No duplicates (600k)1/3 duplicates (800k)1/2 duplicates (900k)
if( !m.containsKey( key ) ){
m.put( key, val );
}
3820ms
3591, 3635, 3549, 4645, 3682
4312ms
4314, 4071, 4179, 4863, 4132
4342ms
4133, 4152, 4795, 4453, 4178
m.put( key, val );3162ms (-17%)
3004, 3920, 3191, 2781, 2914
3935ms (-9%)
3863, 3624, 3957, 3906, 4323
4313ms (-1%)
4174, 4332, 3996, 4205, 4856
  • Calling containsKey() to prevent duplicate put-assignments, will, logically, only become attractive when there are more duplicate values;
  • If there are less than 50% duplicate key-values, this will only introduce another method call (containsKey()) to be performed in addition to the put-assignment;
  • Thus, if one expects a significant volume of ‘prevention’ this could rectify a containsKey()-condition for all iterations, else it is more performant to simply assign and accept the duplicates.

Anonymous Apex

List<Integer> l = new List<Integer>();
for( Integer i = 0; i < 600000; i++ ){
    l.add( i );
    if( Math.mod( i, 3 ) == 0 ){ // for Sc1 comment if-statement, Sc3 change 3 in modulo to 2;
        l.add( i );
    }
}
System.debug( '*** Size: '+ l.size() );

ProductSupplierPrice__c psp = new ProductSupplierPrice__c( Product__c = fflib_IDGenerator.generate( Product2.SObjectType ) );
Map<Integer, String> m1 = new Map<Integer, String>();
Map<Integer, String> m2 = new Map<Integer, String>();

// containsKey put
Long t1 = System.now().getTime();
for( Integer i = 0, j = l.size(); i < j; i++ ){
    Integer intr = l[ i ];
    if( !m1.containsKey( intr ) ){
        m1.put( intr, psp.Product__c );
    }
}
System.debug( '*** ContainsKey Put ' + ( System.now().getTime() - t1 ) );

// always put
Long t2 = System.now().getTime();
for( Integer i = 0, j = l.size(); i < j; i++ ){
    Integer intr = l[ i ];
    m2.put( intr, psp.Product__c );
}
System.debug( '*** Always put ' + ( System.now().getTime() - t2 ) );

Multi-level Maps assignment

To construct a nested/multi-level Map, one should always determine whether the first-level key already exists, allowing to get it and assign the put, or whether the first-level should be created first. This implies the following methods are relevant.

  • mapVar.get( key ); needed to be able to assign to the second-level key-value pair
  • mapVar.put( key, value ); needed to create the first-level key-value pair
  • mapVar.containsKey( key ); optional call to validate whether we should get or put the first-level

There is, thus, the option to always perform the get(), possibly returning null/being obsolete, or directly being possible for second-level enrichments.

Setup

Again a simple anonymous apex , but fetching some relational dummy SObject records from the database to prevent the need to construct it. Due to the impact of volume, three setups were validated, the first were 39k WorkOrders with 352 unique Accounts (keys); the second 11k Contacts of which 1.8k unique Accounts (keys); the last one grouped the original WorkOrders by Id, thus having 39k unique keys, and always a null on the first-level get-condition.

Results

Where the previous results were nice, the impact of a different approach does have a higher impact on this multi-level nested Map construction.

  • Regardless of the number of keys, it is thus always beneficial to first retrieve the second-level Map, then perform an existence check to proceed.
 39,094 WorkOrders
352 Accounts (keys)
11,062 Contacts
1,781 Accounts (keys)
39,094 Workorders
39,094
get, null-check, put
Map m2 = m1.get( key );
if( m2 == null ){
m1.put( key, new Map{ key2 => val } );
} else{
m.put( key2, val );
}
294.8ms (-23%)
210 + 237 + 213 + 216 + 259 + 339
82.2ms (-13%)
70 + 83 + 74 + 73 + 111
387ms (-5%)
395 + 416 + 357 + 381 + 386
containsKey, get, put
if( !m.containsKey( key ) ){
m.put( key, new Map{ key2 => val } );
} else{
m.get( key ).put( key2, val );
}
383.2ms
388 + 297 + 289 + 284 + 351 + 307
94.2ms
88 + 92 + 92 + 90 + 109
408ms
408 + 474 + 358 + 384 + 414

Anonymous Apex

List<WorkOrder> wList = [SELECT Id, AccountId, ServiceContractId FROM WorkOrder];
System.debug( '*** Size: '+ wList.size() );

Map<Id, Map<Id, Id>> m1 = new Map<Id, Map<Id, Id>>();
Map<Id, Map<Id, Id>> m2 = new Map<Id, Map<Id, Id>>();
Map<Id, Map<Id, Id>> m3 = new Map<Id, Map<Id, Id>>();

// get-null-put
Long t1 = System.now().getTime();
for( Integer i = 0, j = wList.size(); i < j; i++ ){
    WorkOrder wo = wList[ i ];
    Map<Id, Id> woByServiceContract = m1.get( wo.AccountId );
    if( woByServiceContract == null ){
        m1.put( wo.AccountId, new Map<Id, Id>{ wo.ServiceContractId => wo.Id } );
    } else{
        woByServiceContract.put( wo.ServiceContractId, wo.Id );
    }
}
System.debug( '*** Always get first ' + ( System.now().getTime() - t1 ) );

// containsKey
Long t2 = System.now().getTime();
for( Integer i = 0, j = wList.size(); i < j; i++ ){
    WorkOrder wo = wList[ i ];
    if( !m2.containsKey( wo.AccountId ) ){
        m2.put( wo.AccountId, new Map<Id, Id>{ wo.ServiceContractId => wo.Id } );
    } else{
        m2.get( wo.AccountId ).put( wo.ServiceContractId, wo.Id );
    }
}
System.debug( '*** containsKey ' + ( System.now().getTime() - t2 ) );

System.debug( '*** ' + m1.keySet().size() );

For-loop efficiency

Ending with my personal favorite, and making the biggest implementation impact to end-users: what for-loop setup to apply. There are different ways to loop over a list of Objects, while there is only one truly distinguishing in terms of performance. The following 2 for- and 1 while-loop are taken into comparison:

  1. for( Object o : objList ){}
    The for-each implementation. Easy to read and apply, enables to loop over Set items, and giving a 200-record-batch performance benefit when putting a SOQL query directly as input-list. However, internally, for-each loops are implemented as a while-loop over an Iterator which might slow the performance a bit down…?
  2. for( Integer i = 0, j = objList .size(); i < j; i++ ){}
    The basic-for-loop implementation, where the size is fetched and assigned to j during initiation, to prevent the size to be determined each time a for-iteration is complete.
    • The analyses does make a distinction on how to fetch the SObject from the list: objList[ i ]; vs objList.get( i );
  3. while( i < size ){ i++; }
    Good all while-loop, though could also have been the do-while

Setup

A similar Anonymous Apex structure is setup, to allow you to validate it yourself. A List of 500k Accounts is initiated and looped over by the four different approaches. The record is only retrieved and no additional action is performed.

Results

500k Accounts
for( Object o : objList ){}1239ms
1269 + 1106 + 1127 + 1151 + 1540
for( Integer i = 0, j = objList .size(); i < j; i++ ){
Object o = objList[ i ];
}
331ms (-73%)
339 + 284 + 321 + 313 + 399
-50% compared to .get( i )
for( Integer i = 0, j = objList .size(); i < j; i++ ){
Object o = objList.get( i );
}
672ms (-46%)
653 + 612 + 639 + 655 + 800
while( i < size ){ i++; }519ms (-58%)
504 + 459 + 507 + 499 + 628

Anonymous Apex

List<Account> accList = new List<Account>();
for( Integer i = 0; i < 500000; i++ ){
    accList.add( new Account( Name = 'Acc ' + i ) );
}

Long t1 = System.now().getTime();
for( Account a : accList ){
    // do nothing
}
System.debug( '*** for-in ' + ( System.now().getTime() - t1 ) );

Long t2 = System.now().getTime();
for( Integer i = 0, j = accList.size(); i < j; i++ ){
    Account a = accList[ i ];
    // do nothing
}
System.debug( '*** for-Integer [i] ' + ( System.now().getTime() - t2 ) );

Long t3 = System.now().getTime();
for( Integer i = 0, j = accList.size(); i < j; i++ ){
    Account a = accList.get( i );
    // do nothing
}
System.debug( '*** for-Integer .get(i) ' + ( System.now().getTime() - t3 ) );

Long t4 = System.now().getTime();
Integer i = 0;
Integer size = accList.size();
while( i < size ){
    Account a = accList[ i ];
    // do nothing
    i++;
}
System.debug( '*** while ' + ( System.now().getTime() - t4 ) );

Conclusions

While some ways of writing code, applying naming conventions, spacing syntax etc might be tight to personal and/or project standards; I hope this article clearly indicates that some implementation approaches have some significant performance impact over the others.

Of course, one should always validate his/her own context, as – like mentioned – Sets can only be looped over via the for-each implementation. Though other than that, I believe there are clear results on what to use the next time in such need:

  • for(; i<j; i++) is 73% faster than a for( Obj o : objList );
  • objList[ i ] is 50% faster than using the objList.get( i ) method;
  • For multi-level Map constructions, it’s best to first get() the first-level, validate its existence and allowing using it to enrich the second-level, instead of using containsKey(). Even for the scenario of populating a Map with only unique keys this consumed 5% less time;
  • Although it feels ‘better’ to not ‘duplicate’ a key-value pair assignment to a Map, the containsKey() method causes a slower performance than simply always assigning/overwriting the key-value . While this is also heavily depending on the number of duplicate keys.

Lastly, as disclaimer, all tests were performed 5 times, and the average was taken. Of course, results can vary based on platform connectivity etc, especially in the multi-tenant Salesforce setup. Though it is expected that the relative comparisons will be comparable at different times and different instances.

How useful was this post?

Average rating 0 / 5. Vote count: 0

No votes so far! Be the first to rate this post.

We are sorry that this post was not useful for you!

Let us improve this post!

Tell us how we can improve this post?

Leave a comment

Your email address will not be published. Required fields are marked *