17

Considering the following test class:

@IsTest
private class ProfilingTest {

    private static String TEST_STR = '';

    public static testMethod void testForEachLoop(){
        List<Account> accs = [SELECT Id, Name FROM Account LIMIT 5000];

        for(Account acc : accs){
            acc.Name += TEST_STR;
        }
    }


    public static testMethod void testForLoop(){
        List<Account> accs = [SELECT Id, Name FROM Account LIMIT 5000];

        for(Integer i = 0; i < accs.size(); ++i){
            accs[i].Name += TEST_STR;
        }
    }


    public static testMethod void testImprovedForLoop(){
        List<Account> accs = [SELECT Id, Name FROM Account LIMIT 5000];
        Integer size = accs.size();

        for(Integer i = 0; i < size; ++i){
            accs[i].Name += TEST_STR;
        }
    }


    public static testMethod void testWhileLoop(){
        List<Account> accs = [SELECT Id, Name FROM Account LIMIT 5000];
        Integer size = accs.size();
        Integer i = 0;

        while(i < size){
            accs[i].Name += TEST_STR;
            ++i;
        }
    }
}

And the test result : enter image description here

Is there a particular reason why standard for loop is faster in Apex than for-each loop? What I think is it may be because the 'i' knows which element to pick and for every iteration the size of the list gets smaller and smaller.

But I need some more explanation as why are we seeing huge performance advantage when using standard for loop ?

2
  • As an aside, not really worth being part of the answer, this test shows that all three methods are approximately equal (besides the larger for-each loop). +/- 10% is pretty much explained by system load and latency, and shouldn't be considered as for which of the three other loops are the fastest (depending on circumstances, may change each run).
    – sfdcfox
    Aug 1, 2018 at 2:41
  • Out of curiosity, what results do you get if you change the concatenation from acc.Name += TEST_STR; to accs[index++].Name += TEST_STR; in the testForEachLoop method with index defined outside the loop as Integer index=0.
    – Jayant Das
    Aug 1, 2018 at 3:45

4 Answers 4

13

It's because for each loops end up using an Iterator, which is inherently slower than a normal for loop. Here's what it might compile to internally:

Iterator<Object> iter = source.iterator();
while(iter.hasNext()) {
  Object next = iter.next();
  forLoopBody.yield(next);
}

As you can see, it has to initialize an extra object, and has to do a bit more work than a straight for loop (calling not just one, but two functions for every value in the loop). This effect is multiplied when you have a for each loop nested inside another, as it has to create a new Iterator for every value in the outer loop, even if the Iterator is empty.

I don't have the exact timing of this, as it is a small, but noticeable difference. There are several losses, including having to allocate an entire object instead of a simple variable, and the overhead of calling methods.

For now, while for each loops are easier to read, they are definitely the worst in performance, especially compared to a pre-initialized for loop:

for(Integer index = 0, size = someList.size(); index < size; index++) {

As other answers noted, you may be also be seeing database latency. Here's an updated version that doesn't use the database:

@isTest class q227272 {
    private static String TEST_STR = '';

    static Account[] createAccounts() {
        Account[] records = new Account[0];
        for(Integer i = 0; i < 5000; i++) {
            records.add(new Account(Name=''+i));
        }
        return records;
    }
    public static testMethod void testForEachLoop(){
        List<Account> accs = createAccounts();
        Long start = DateTime.now().getTime();

        for(Account acc : accs){
            acc.Name += TEST_STR;
        }
        System.assert(false, DateTime.now().getTime()-start);
    }


    public static testMethod void testForLoop(){
        List<Account> accs = createAccounts();
        Long start = DateTime.now().getTime();

        for(Integer i = 0; i < accs.size(); ++i){
            accs[i].Name += TEST_STR;
        }
        System.assert(false, DateTime.now().getTime()-start);
    }


    public static testMethod void testImprovedForLoop(){
        List<Account> accs = createAccounts();
        Long start = DateTime.now().getTime();
        Integer size = accs.size();

        for(Integer i = 0; i < size; ++i){
            accs[i].Name += TEST_STR;
        }
        System.assert(false, DateTime.now().getTime()-start);
    }


    public static testMethod void testWhileLoop(){
        List<Account> accs = createAccounts();
        Long start = DateTime.now().getTime();
        Integer size = accs.size();
        Integer i = 0;

        while(i < size){
            accs[i].Name += TEST_STR;
            ++i;
        }
        System.assert(false, DateTime.now().getTime()-start);
    }
}

After ignoring the effects of the database, we get output like this:

testForEachLoop      93ms
testForLoop          104ms
testImprovedForLoop  73ms
testWhileLoop        80ms

Which still shows that the improved for loop is the best option, but the testForLoop takes the worst place; calling size() each iteration results in undesirable performance, while caching the size() call is the best option.

2
  • Would using a soql for loop provide any benefit?
    – Girbot
    Aug 1, 2018 at 5:53
  • 2
    @Girbot SOQL in a for loop trades CPU time for heap size. That is, if you're running into memory issues, you can trade some CPU in exchange (more CPU time for less heap). There are definitely times when you can/should use SOQL-for loops.
    – sfdcfox
    Aug 1, 2018 at 9:58
4

Great to see a question with concrete data included.

I'm surprised the difference is so large. I strongly suggest you re-order the methods and see if that makes a difference. You have the query within your timings and the first query will cache the data - at least at the Oracle level - and so subsequent ones will be quicker.

(I don't see a SeeAllData=true so where are the Accounts coming from? If you are using a very low API number so that isn't needed, it would be worth moving to the latest API number.)

I'd like to see this case included too:

public static testMethod void testSoqlForEachLoop(){
    for(Account acc : [
            SELECT Id, Name
            FROM Account
            LIMIT 5000
            ]){
        acc.Name += TEST_STR;
    }
}

which is a pattern I use all the time and aims to be kind to heap space usage.

I favour clean code over (small) performance differences. If code needs tuning to the level of changing from one closely equivalent algorithm to another, you are probably bumping up to and will exceed the CPU governor limit at some point in the future when more code has to be added.

A key thing is to avoid exponential time growth algorithms and instead refactor to algorithms where the time growth is linear e.g. use a map instead of a nested loop. See e.g. Fixing a common cause of System.LimitException: Apex CPU time limit exceeded.

0

Interesting question. My random guess would be because you initialize a new object(OO talk here) in a foreach loop and do not do it in the other ones.

If you try the same test class but with initializing new Accounts inside your for loops, you will probably have more accurate data.

For example:

public static testMethod void testForLoop(){
    List<Account> accs = [SELECT Id, Name FROM Account LIMIT 5000];

    for(Integer i = 0; i < accs.size(); ++i){
        Account a = accs.get(i);
        a.Name += TEST_STR;
    }
}

I will also try it myself when I have time.

2
  • That code is no different to the array case. In Apex/Java getting an element from a list or array does not involve any memory allocation as a reference is returned not a copy.
    – Keith C
    Aug 1, 2018 at 9:32
  • 2
    Specifically, I recommend never using the get method. It's at least twice as slow as using a direct index (Account a = accs[i];) for no discernible benefit. The main benefit is Java portability.
    – sfdcfox
    Aug 1, 2018 at 10:04
0

Your test methodology and result are flawed because of the database caching (connection caching and query caching) that takes place. The first test to run will always be the slowest to run because loading the data takes way more time than iterating on results.

The only way to properly compare these iteration methods is to exclude the query from the test or to defeat the caching. I suggest that you create a helper method that returns a list of accounts that you build locally yourself.

1
  • 1
    FWIW, I set up a test that uses "testSetup" and the results are different (but still expected: 151, 113, 98, 103 ms for those tests in the same order). for-each loops are still the slowest, but with cached results, the difference is only 50% more and not 300% more.
    – sfdcfox
    Aug 1, 2018 at 10:16

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .