22

The focus of this question is how the Apex compiler/runtime behave with maps that include static initialization data. So assuming a map of fixed values needs to be setup purely in Apex...

The simplest way to code that is like this:

private static final Map<String, String> ISO_TO_SYMBOL = new Map<String, String>{
        'USD' => '$', 
        'CAD' => '$',
        'EUR' => '€',
        'GBP' => '£',
        'JPY' => '¥',
        'KRW' => '₩',
        'CNY' => '元',
        ...
        };

A more convoluted way is:

private static Map<String, String> ISO_TO_SYMBOL {
    get {
        if (ISO_TO_SYMBOL == null) {
            ISO_TO_SYMBOL = new Map<String, String>{
                    'USD' => '$', 
                    'CAD' => '$',
                    'EUR' => '€',
                    'GBP' => '£',
                    'JPY' => '¥',
                    'KRW' => '₩',
                    'CNY' => '元',
                    ...
                    };
            }
        }
        return ISO_TO_SYMBOL;
    }
    set;
}

A reason to take the second approach would be if the map initialisation was done at run time rather than compile time so cost was incurred every time the class was loaded (and only some methods in the class used the data so those that didn't would bear a needless cost). Also consider that if many classes are loaded to process a request, there could be many such maps resulting in hundreds or thousands of map puts being needlessly done.

Does anyone have an evidence-backed view on which pattern is best in Apex?

PS

It occurred to me to look at the (finest level) debug log for a test case:

@IsTest
private class MapTest {

    private static final Map<String, String> M1 = new Map<String, String>{
        'abc' => '123',
        'def' => '456'
    };

    private static Map<String, String> M2 {
        get {
            if (M2 == null) M2 = new Map<String, String>{
                    'PQR' => '987',
                    'STU' => '654'
                    };
            return M2;
        }
        set;
    }

    @IsTest
    static void test() {
        // Touches neither map
    }
}

which yielded:

07:17:02.0 (434345)|CODE_UNIT_STARTED|[EXTERNAL]|01p46000006cDFz|MapTest.test
07:17:02.0 (652740)|VARIABLE_SCOPE_BEGIN|[4]|MapTest.M1|Map<String,String>|true|true
07:17:02.0 (813407)|VARIABLE_SCOPE_BEGIN|[9]|MapTest.M2|Map<String,String>|true|true
07:17:02.0 (978657)|HEAP_ALLOCATE|[72]|Bytes:3
07:17:02.0 (1039411)|HEAP_ALLOCATE|[77]|Bytes:152
07:17:02.0 (1059294)|HEAP_ALLOCATE|[342]|Bytes:408
07:17:02.0 (1079287)|HEAP_ALLOCATE|[355]|Bytes:408
07:17:02.0 (1096414)|HEAP_ALLOCATE|[467]|Bytes:48
07:17:02.0 (1126412)|HEAP_ALLOCATE|[139]|Bytes:6
07:17:02.0 (1142006)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:84
07:17:02.0 (1169464)|HEAP_ALLOCATE|[EXTERNAL]|Bytes:1
07:17:02.0 (1184616)|METHOD_ENTRY|[2]|01p46000006cDFz|MapTest.MapTest()
07:17:02.0 (1189674)|STATEMENT_EXECUTE|[2]
07:17:02.0 (1195887)|STATEMENT_EXECUTE|[2]
07:17:02.0 (1197317)|STATEMENT_EXECUTE|[4]
07:17:02.0 (1204261)|HEAP_ALLOCATE|[4]|Bytes:4
07:17:02.0 (1216226)|HEAP_ALLOCATE|[5]|Bytes:3
07:17:02.0 (1219723)|HEAP_ALLOCATE|[5]|Bytes:3
07:17:02.0 (1278906)|HEAP_ALLOCATE|[6]|Bytes:3
07:17:02.0 (1283649)|HEAP_ALLOCATE|[6]|Bytes:3
07:17:02.0 (1334734)|VARIABLE_ASSIGNMENT|[4]|MapTest.M1|{"abc":"123","def":"456"}|0x4d6865da
07:17:02.0 (1345930)|METHOD_EXIT|[2]|MapTest
07:17:02.0 (1382667)|HEAP_ALLOCATE|[50]|Bytes:5
07:17:02.0 (1411089)|HEAP_ALLOCATE|[56]|Bytes:5
07:17:02.0 (1418801)|HEAP_ALLOCATE|[64]|Bytes:7
07:17:02.0 (1459133)|STATEMENT_EXECUTE|[21]

I interpret the VARIABLE_ASSIGNMENT in that to mean that a reference to an already created (at compile time) map is being assigned. Touching M2 in the test results in a similar VARIABLE_ASSIGNMENT. So I'm starting to conclude that the simplest coding is the way to go for static initialization data.

  • 3
    What about custom metadata types? I try to avoid defining static definitions like these in code. Plus you get the benefit of being able to update in a declarative manner. – Adam M Jun 28 '17 at 13:25
  • @AdamMcCardle Fair point. Presumably lazy loading is a must in that case. My example might not be the best: I'll add an assumption to the question that the map is something that will never change. – Keith C Jun 28 '17 at 13:32
  • We had a similar requirement where we needed to load some static constant values for both client(Lightning components) and server side. - Server Side: From a static class we accessed a static resource which contained information in the form of a json. - client side: we have a post login Lightning component, we added the same static resource there and accessed it in init of the component and stored the value in session storage which we used across the session. It kind of helped in both places – RedDevil Jun 28 '17 at 13:44
  • 1
    @RedDevil Yes I agree with you that JSON static resources can work very well. I'm trying to focus on quite a narrow question here though of how the Apex compiler/runtime behave. – Keith C Jun 28 '17 at 13:50
  • @KeithC, My apologies i didnt catch your question completely – RedDevil Jun 28 '17 at 13:51
16

The pattern used depends on if you expect the map to be used or not. You have a misconception here, about "compile time vs execution time." Unlike Java, there is no optimization in Apex Code that serializes statically initialized maps. In other words, it always executes every time, and costs CPU time accordingly. The difference between the first and second method is that the first one will always cost CPU time, while the second will only cost CPU time if that property is accessed (lazy loading). However, if statements themselves have some non-zero CPU usage, so if you need to use the map a lot (say, in a large loop), the first method will have less execution time per loop.

In one extreme case I had, I had a class that was used by many other classes, and had hundreds of final static variables; we timed it and found that the class added an entire 1,000 ms to every transaction that used that class. We converted the class to the second form (lazy-loading using getters), and it reduced the average transaction time by about 990 ms for any class that used it. In other words, you should definitely avoid loading data that you may or may not use. Using the second form typically saves a lot of CPU time.

  • I have assumed there isn't optimization, but the single VARIABLE_ASSIGNMENT in the log at the end of the question is starting to make me think that there is. How do you interpret that? – Keith C Jun 28 '17 at 15:40
  • @KeithC The unit test doesn't start until STATEMENT_EXECUTE|[21]; the lines above it show that lines 2-4 are executed, which first triggers some heap allocations, followed by the variable assignment, then the "static initialization code" finishes (METHOD_EXIT), before finally starting the unit test STATEMENT_EXECUTE|[21]. Note that lines 9-18 are not referenced in the logs because they never execute. – sfdcfox Jun 28 '17 at 15:49
  • agreed. Using static final constants in a class means that whenever a class is being accessed, all the variables will be loaded irrespective whether that transaction requries all of the constants or not. Making them as only static and using an access method and combine with lazy loading will same considerable loading time. – psun Dec 6 '17 at 21:25
2

While I suppose you could use lazy loading to shave CPU time, the most consistent and obvious savings for me has always been in reducing other governors such as queries, callouts, etc.

For example:

public static Map<String, String> configData1
{
    get
    {
        if (configData1 == null)
        {
            configData1 = new Map<String, String>();
            for (ConfigObject1__c record : [SELECT ... FROM ConfigObject1__c])
                configData1.put(record.Name, record.SomeOtherField__c);
        }
        return configData1;
    }
    private set;
}
public static Map<String, String> configData2
{
    get
    {
        if (configData2 == null)
        {
            configData2 = new Map<String, String>();
            for (ConfigObject1__c record : /*callout result*/)
                configData2.put(record.Name, record.SomeOtherField__c);
        }
        return configData2;
    }
    private set;
}

Imagine if you call some static method on your class which doesn't care about configData2, you would be quite surprised at the side effect that calling the method consumes a callout. Same could be said of config1Data with the query, and if you have several of these maps, it can add up.

Another side benefit is that you can make your collections private set so you know they are not nillable. Although the same can be achieved by declaring them final.

  • Yeah, but your examples are of data that isn't static so isn't known at compile time. I'm trying to get at quite a niche case... Agree that lazy loading provides a huge advantage where an expensive operation is involved and where not all values are needed. – Keith C Jun 28 '17 at 15:44
  • Hmm, yeah I keep reading static as the keyword static. I was getting at a pretty similar point as sfdcfox in the end. – Adrian Larson Jun 28 '17 at 15:45
0

A static map like your example would not take much processing time and would not matter much in the overall flow of your app. The amount of data would have to be significant to make a dent in processing time. My only concern would be if there were complex queries to the database or calls outside of Salesforce. Which would be similar to the comment about custom metadata types and lazy loading should be setup. Another concern I have is if the static definitions in the code are large enough, they will start affecting the apex character count for the org.

I would personally make static definitions process on compile time because of the low impact it has and because of the simplicity, even if the map isn't used very much.

  • Fair point that other initialization is way more expensive. Just trying to tie down the specifics of this sort of initialization based on evidence. – Keith C Jun 28 '17 at 14:04
  • What kind of evidence are you seeking? Besides a large swath of personal experiences and preference, you would need to have some analytics built around the different methodologies. I would put one data point on total processing time of page loads and method calls. Another point would be to analyze how often the class holding the map is loaded vs how often the map is used. Using some statistical trickery using some of these points could help gauge the impact. I feel like this is very dependent on the situation. – Adam M Jun 28 '17 at 14:42
  • Looks like you started some analysis already ^^ – Adam M Jun 28 '17 at 14:49
  • 2
    Yeah I think I've more or less answered my own question and a vaguely remember this kind of compile-time optimisation in Java though no static initialisation there. – Keith C Jun 28 '17 at 15:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.