How to avoid instantiating object inside a loop?
Recommendation
Object instantiation is fairly cheap. However, you can make it more efficient in two ways:
- Set field values using name/value pairs.
- 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.
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)
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!