How to avoid instantiating object inside a loop?

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.


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!