18

There are many scenarios where I have to create records of polymorphic objects using apex. For e.g. creating a Task record or a CustomObject__c record for each Case created. In general I do something like below in Apex:

for(case c:createdCases)
{
    task t = new task();
    t.OwnerId = somemap.get(c.Id);
    t.subject = 'Please Close Case # '+c.CaseNumber;
    t.Priority = 'High';
    t.Status = 'Open';
    t.whatId = c.Id;
    t.ActivityDate = Date.TODAY().addDays(3);
    t.Description = 'Please close this case before the due date - '+ Date.TODAY().addDays(3).format();
    taskList.add(t);

}

insert taskList;

I would love to know if task t = new task();or customObj__c obj = new customObj__c; inside a for loop is a costly affair. If yes, does it going to add up significantly to the heap size for the execution context. How can we avoid this?

4 Answers 4

25

Recommendation

Object instantiation is fairly cheap. However, you can make it more efficient in two ways:

  1. Set field values using name/value pairs.
  2. Don't cache the object, just add it directly to the list.

So that would look like:

for (Case record : createdCases)
{
    tasks.add(new Task(
        OwnerId=someValue,
        Subject='Some other value',
        Priority='etc.'
    ));
}

Profiling

I did some profiling to figure out how these two factors affect CPU cost. I did ten runs of one trial of each type laid out below. Subsequent runs were much faster, so I excluded them from my results (or rather stopped running them).

TL;DR

Most of the cost you can make up is in the name/value pairs. With the removal of caching having a negligible effect on CPU consumption, that aspect seems primarily stylistic.

Tabular Format

Operation         Average    Minimum    Maximum
Empty                64.0         56         74
Efficient           477.0        432        516
Caching             482.1        438        581
Setting Fields      555.1        512        664

Empty Loop Cost

First, I profiled an empty loop so I can subtract out the operations we don't care about. Something like:

final Integer COUNT = 100;
List<Account> records = [SELECT OwnerId FROM Account LIMIT :COUNT];
Long start = Datetime.now().getTime();
for (Integer i = 0; i < COUNT; i++)
{
    List<Task> tasks = new List<Task>();
    for (Account record : records) continue;
}
system.debug(Datetime.now().getTime() - start);

On average, this loop took 64ms, with a minimum run time of 56ms and a maximum run time of 74ms. That means that we can assume it costs less than 1ms to instantiate the List<Task> and iterate through the Account records a single time.

Efficient Loop Cost

Next I checked out the performance of my recommended loop refactor.

final Integer COUNT = 100;
List<Account> records = [SELECT OwnerId FROM Account LIMIT :COUNT];
Long start = Datetime.now().getTime();
for (Integer i = 0; i < COUNT; i++)
{
    List<Task> tasks = new List<Task>();
    for (Account record : records)
        tasks.add(new Task(
            OwnerId=record.OwnerId, WhatId=record.Id
        ));
}
system.debug(Datetime.now().getTime() - start);

Average: 477ms, Minimum: 432ms, Maximum: 516ms.

Record Caching

final Integer COUNT = 100;
List<Account> records = [SELECT OwnerId FROM Account LIMIT :COUNT];
Long start = Datetime.now().getTime();
for (Integer i = 0; i < COUNT; i++)
{
    List<Task> tasks = new List<Task>();
    for (Account record : records)
    {
        Task newTask = new Task(
            OwnerId=record.OwnerId, WhatId=record.Id
        );
        tasks.add(newTask);
    }
}
system.debug(Datetime.now().getTime() - start);

Average: 482.1ms, Minimum: 438ms, Maximum: 581ms.

Setting Individual Fields Cost

final Integer COUNT = 100;
List<Account> records = [SELECT OwnerId FROM Account LIMIT :COUNT];
Long start = Datetime.now().getTime();
for (Integer i = 0; i < COUNT; i++)
{
    List<Task> tasks = new List<Task>();
    for (Account record : records)
    {
        Task newTask = new Task();
        newTask.OwnerId = record.OwnerId;
        newTask.WhatId = record.Id;
        tasks.add(newTask);
    }
}
system.debug(Datetime.now().getTime() - start);

Average: 555.1ms, Minimum: 512ms, Maximum: 664ms.

3
  • Did you attend Dan Appleman's Dark Art of CPU Benchmarking at DF16 too? I'm actually surprised that you came up with a significantly different figure than I did between inline constructor in loop and setting fields with dot notation. The main difference between us is that you used Long and DateTime.now.getTime() whereas I was using Decimal and Limits.getCPUTime(). Is there a clear preference of one over the other in benchmarks like this?
    – Derek F
    Commented Oct 11, 2016 at 15:47
  • I did, but really he didn't cover anything that wasn't in his book. One of the askers did note that using Visualforce will reduce noise, though, so I have adopted that approach. I prefer examining the real time interval, rather than CPU Time. It just seems more reliable to me.
    – Adrian Larson
    Commented Oct 11, 2016 at 15:54
  • I know nothing of Apex... is a List stored continuously in memory? If so, then many iterations of add will cause allocating more space and a potential move of the entire List. Obviously not, if it's a "linked list" type structure. I'd like to see a benchmark where you instantiate the List with as many Tasks as you need, and then loop through while assigning values to each one.
    – Carl
    Commented Oct 12, 2016 at 4:31
13

As Adrian Larson pointed out, object instantiation is pretty cheap.

One pattern that I've used in some places is to create a base instance outside of a loop, setting as many common fields as possible, and then clone the base instance inside the loop, setting specific fields only where required.

Task baseTask = new Task(
    ActivityDate = Date.TODAY().addDays(3),
    Prioity = 'High'
    // ...other common fields here
);

Task cloneTask;
for (Case record : createdCases)
{
    cloneTask = baseTask.clone(false, true, false, false);
    cloneTask.whatId = record.Id;
    tasks.add(cloneTask);
}

I've no idea how performant the sObject clone() method is (I should probably benchmark that), but I do know for a fact that using object.field = value is slower than setting fields via name/value pairs in the sObject constructor.

At any rate, this is unlikely to impact you unless you're attempting to get close to the 10,000 DML row limit per transaction.

+edit:

wrote up a benchmarking script

Decimal time1;
Decimal time2;
Integer iterations = 20000;

Decimal bareLoop;
Decimal instantiateInLoop;
Decimal cloneIntoList;
Decimal cloneInLoop;
Decimal cloneInLoopAndSet1Field;
Decimal cloneInLoopAndSet2Fields;
Decimal clone3Fields;
Decimal clone4Fields;

time1 = Limits.getCpuTime();
for(Integer i = 0; i < iterations; i++){
}
time2 = Limits.getCpuTime();
bareLoop = time2-time1;

List<Opportunity> testOppList = new List<Opportunity>();
time1 = Limits.getCpuTime();
for(Integer i = 0; i < iterations; i++){
    testOppList.add(new Opportunity(
        description = 'test description',
        stageName = '1 - Working',
        Amount = i,
        CloseDate = Date.Today().addDays(3)
    ));
}
time2 = Limits.getCpuTime();
instantiateInLoop = time2-time1 - bareLoop;

testOppList.clear();
Opportunity baseInstance;
Opportunity cloneInstance;
time1 = Limits.getCpuTime();
baseInstance = new Opportunity(
    description = 'test description',
    stageName = '1 - Working',
    CloseDate = Date.Today().addDays(3)
);

for(Integer i = 0; i < iterations; i++){
    testOppList.add(baseInstance.clone(false, true, false, false));
}
time2 = Limits.getCpuTime();
cloneIntoList = time2-time1 - bareLoop;
testOppList.clear();

time1 = Limits.getCpuTime();
baseInstance = new Opportunity(
    description = 'test description',
    stageName = '1 - Working',
    CloseDate = Date.Today().addDays(3)
);
for(Integer i = 0; i < iterations; i++){
    cloneInstance = baseInstance.clone(false, true, false, false);
    testOppList.add(cloneInstance);
}
time2 = Limits.getCpuTime();
cloneInLoop = time2-time1 - bareLoop;
testOppList.clear();

time1 = Limits.getCpuTime();
baseInstance = new Opportunity(
    description = 'test description',
    stageName = '1 - Working',
    CloseDate = Date.Today().addDays(3)
);
for(Integer i = 0; i < iterations; i++){
    cloneInstance = baseInstance.clone(false, true, false, false);
    cloneInstance.Amount = i;
    testOppList.add(cloneInstance);
}
time2 = Limits.getCpuTime();
cloneInLoopAndSet1Field = time2-time1 - bareLoop;
testOppList.clear();

time1 = Limits.getCpuTime();
baseInstance = new Opportunity(
    description = 'test description',
    stageName = '1 - Working',
    CloseDate = Date.Today().addDays(3)
);
for(Integer i = 0; i < iterations; i++){
    cloneInstance = baseInstance.clone(false, true, false, false);
    cloneInstance.Amount = i;
    cloneInstance.Name = 'Opp-' + i;
    testOppList.add(cloneInstance);
}
time2 = Limits.getCpuTime();
cloneInLoopAndSet2Fields = time2-time1 - bareLoop;
testOppList.clear();

baseInstance = new Opportunity(
    description = 'test description',
    stageName = '1 - Working',
    CloseDate = Date.Today().addDays(3)
);
time1 = Limits.getCpuTime();
for(Integer i = 0; i < iterations; i++){
    testOppList.add(baseInstance.clone(false, true, false, false));
}
time2 = Limits.getCpuTime();
clone3Fields = time2-time1 - bareLoop;
testOppList.clear();

baseInstance = new Opportunity(
    description = 'test description',
    stageName = '1 - Working',
    Amount = 100,
    CloseDate = Date.Today().addDays(3)
);

time1 = Limits.getCpuTime();
for(Integer i = 0; i < iterations; i++){
    testOppList.add(baseInstance.clone(false, true, false, false));
}
time2 = Limits.getCpuTime();
clone4Fields = time2-time1 - bareLoop;
testOppList.clear();

system.debug('Time taken in bare loop (just instantiating, comparing, and incrementing i): ' + bareLoop);
system.debug('Time taken directly adding new instance to list (minus bareLoop): ' + instantiateInLoop);
system.debug('Time taken cloning instance direcly into list (minus bareLoop): ' + cloneIntoList);
system.debug('Time taken cloning instance direcly into list, 3 fields (minus bareLoop): ' + clone3Fields);
system.debug('Time taken cloning instance direcly into list, 4 fields (minus bareLoop): ' + clone4Fields);
system.debug('Time taken cloning instance direcly into list, per record, 1 extra field (minus bareLoop): ' + ((clone4Fields - clone3Fields)/iterations));
system.debug('Time taken cloning, then adding instance to list (minus bareLoop): ' + cloneInLoop);
system.debug('Time taken cloning, setting 1 field, then adding instance to list (minus bareLoop): ' + cloneInLoopAndSet1Field);
system.debug('Time taken cloning, setting 2 fields, then adding instance to list (minus bareLoop): ' + cloneInLoopAndSet2Fields);
system.debug('Time taken (per record) to set 1 field using dot notation (minus bareLoop): ' + ((cloneInLoopAndSet1Field - cloneInLoop)/iterations));
system.debug('Time taken (per record) to set an additional field using dot notation (minus bareLoop): ' + ((cloneInLoopAndSet2Fields - cloneInLoopAndSet1Field)/iterations));

results (20,000 iterations, note that there will be some non-deterministic variance between runs):

Time taken in bare loop (just instantiating, comparing, and incrementing i): 11

Time taken directly adding new instance to list (minus bareLoop): 672

Time taken cloning instance direcly into list (minus bareLoop): 331

Time taken cloning instance direcly into list, 3 fields (minus bareLoop): 334

Time taken cloning instance direcly into list, 4 fields (minus bareLoop): 373

Time taken cloning instance direcly into list, per record, 1 extra field (minus bareLoop): 0.00195

Time taken cloning, then adding instance to list (minus bareLoop): 354

Time taken cloning, setting 1 field, then adding instance to list (minus bareLoop): 970

Time taken cloning, setting 2 fields, then adding instance to list (minus bareLoop): 1459

Time taken (per record) to set 1 field using dot notation (minus bareLoop): 0.0312

Time taken (per record) to set an additional field using dot notation (minus bareLoop): 0.02445

I did a separate test to see what the incremental cost was to an additional field being set in the constructor.

Cost per record per additional field instantiating in loop: 0.01655

Conclusions:

  • Cloning is fast, roughly half the CPU cost of repeatedly making new instances and setting name/value pairs in the constructor (even when storing in a temp variable
  • Cloning should always remain faster than repeated constructor calls, as the incremental cost for cloning an additional field is an order of magnitude (i.e. 10x) lower
  • This benefit disappears as soon as you need to set even a single value on a record using dot-notation
  • There appears to be no number of fields that you can set via constructor that would cause cloning + dot notation to be favorable (dot notation cost is ~2x that of setting an additional field in the constructor)
6
  • Have you done any profiling? I would imagine clone itself to be fairly expensive, but I've never checked.
    – Adrian Larson
    Commented Oct 11, 2016 at 14:39
  • 1
    @AdrianLarson I'm working on it now. Early results are that cloning itself is pretty cheap.
    – Derek F
    Commented Oct 11, 2016 at 14:43
  • Seems way too high a price to pay in code cleanliness unless the performance gain is significant and important. And would certainly need a comment explaining hat...
    – Keith C
    Commented Oct 11, 2016 at 15:00
  • 2
    I'm pretty sure cloning is cheaper than inline constructors when many of the fields are identical. The hardest hit on performance is assigning the fields that are not unique.
    – sfdcfox
    Commented Oct 11, 2016 at 15:13
  • 4
    @DerekF Great to have the data. Also worth remembering ideas like "Programmers waste enormous amounts of time thinking about, or worrying about, the speed of noncritical parts of their programs, and these attempts at efficiency actually have a strong negative impact when debugging and maintenance are considered. We should forget about small efficiencies, say about 97% of the time: premature optimization is the root of all evil. Yet we should not pass up our opportunities in that critical 3%". Suspect the code in the question is part of the 97%...
    – Keith C
    Commented Oct 11, 2016 at 15:51
10

In addition to @AdrianLarson's answer, I did a little digging on this for what it's worth using the following code:

System.debug('Start: ' + System.now());

List<Contact> contactList = new List<Contact>();

for (Integer i = 0; i < 2000; i++) {
  Contact con = new Contact(
    FirstName = 'Foo' + i,
    LastName = 'Bar'
  );

  contactList.add(con);
}

System.debug('Finish: ' + System.now());

This returned the following:

15:08:19.30 (31134544)|USER_DEBUG|[1]|DEBUG|Start: 2016-10-11 14:08:19

15:08:19.30 (85516226)|USER_DEBUG|[10]|DEBUG|Finish: 2016-10-11 14:08:19

And when I did the same thing using the other method:

System.debug('Start: ' + System.now());

List<Account> accountList = new List<Account>();

for (Integer i = 0; i < 2000; i++) {
  Account acc = new Account();

  acc.Name = 'Foo Bar ' + i;

  accountList.add(acc);
}

System.debug('Finish: ' + System.now());

Returned:

15:12:09.19 (20452341)|USER_DEBUG|[1]|DEBUG|Start: 2016-10-11 14:12:09

15:12:09.19 (117639487)|USER_DEBUG|[13]|DEBUG|Finish: 2016-10-11 14:12:10

And finally...

System.debug('Start: ' + System.now());

List<Task> taskList = new List<Task>();

for (Integer i = 0; i < 2000; i++) {
  taskList.add(new Task(
    Subject='Foo Bar'
  ));
}

System.debug('Finish: ' + System.now());

15:17:12.20 (21014329)|USER_DEBUG|[1]|DEBUG|Start: 2016-10-11 14:17:12

15:17:12.20 (59016945)|USER_DEBUG|[11]|DEBUG|Finish: 2016-10-11 14:17:12

So when Adrian says:

Object instantiation is fairly cheap.

He isn't kidding.

In fact, I had to instantiate 200,000 records just to get a 6ms difference between the Start and Finish debug!

4
  • 1
    Beat me to the profiling. I'll probably still run some of my own just to complete the answer. Nice work though! +1
    – Adrian Larson
    Commented Oct 11, 2016 at 14:39
  • @AdrianLarson I'd be interested to see what you get. I've always found doing something like Contact con = new Contact(); con.FirstName='Foo'; con.Lastname = 'Bar'; in my experience tends to take longer although the difference isn't huge.
    – Dan Jones
    Commented Oct 11, 2016 at 14:42
  • 5
    When profiling code, it's usually worth it to use the following syntax: Long startTime = System.now().getTime(); /* code to profile */ Long endTime = System.now().getTime(); System.debug(endTime-startTime); This gives you millisecond precision, which is a lot more valuable for determining the amount of time used.
    – sfdcfox
    Commented Oct 11, 2016 at 15:17
  • Thanks @sfdcfox that's really useful to know. I'll keep that in mind!
    – Dan Jones
    Commented Oct 11, 2016 at 20:02
8

Salesforce emphasise the importance of doing database operations in bulk which pretty much forces you to build collections of objects in memory and so to consume heap. Heap allocation is well optimised at this point after 20+ years of languages like Java so shouldn't be on your mind as a performance issue. And in most Apex code it shouldn't be on your mind as a governor limit problem either.

But if you know you are going to be processing large numbers (thousands) of records, Apex provides two off the shelf mechanisms to help avoid the heap governor limit...

1) The list form of the SOQL For Loop:

for (variable_list : [soql_query]) {
    code_block
}

that:

The sObject list format executes the for loop's once per list of 200 sObjects. Consequently, it is a little more difficult to understand and use, but is the optimal choice if you need to use DML statements within the for loop body. Each DML statement can bulk process a list of sObjects at a time.

2) Moving to asynchronous processing via batch Apex.

You must log in to answer this question.

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