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 thekeyalready exists, by always first checkingcontainsKey(); - 2. Always
putall values, potentially overwriting and performing same action multiple times.
- 1. Prevent
Results
The results are rather logical when interpreting:
| No duplicates (600k) | 1/3 duplicates (800k) | 1/2 duplicates (900k) | |
if( !m.containsKey( key ) ){ | 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 duplicateput-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 theput-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 pairmapVar.put( key, value );needed to create the first-level key-value pairmapVar.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, putMap m2 = m1.get( key ); | 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, putif( !m.containsKey( key ) ){ | 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:
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…?for( Integer i = 0, j = objList .size(); i < j; i++ ){}
The basic-for-loop implementation, where the size is fetched and assigned tojduring 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 ];vsobjList.get( i );
- The analyses does make a distinction on how to fetch the SObject from the list:
while( i < size ){ i++; }
Good all while-loop, though could also have been thedo-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 theobjList.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 usingcontainsKey(). 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 thekey-value. While this is also heavily depending on the number of duplicatekeys.
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.