views:

452

answers:

9

I have a method which summarizes a collection of decimal values using a method similar to this...

Dim _amountCollection as New List(Of Decimal)
_amountCollection.Add(145.12D)
_amountCollection.Add(11.32D)
_amountCollection.Add(6547.07D)

Dim _totalAmount as Decimal

For Each _individualAmount as Decimal in _amountCollection
  _totalAmount += _individualAmount
Next

In the actual code, there are usually more members in the amount collection, and there are at least 50 individual amount collections that need to be summed in a total operation.

This recalculation of total amount gets called often (at least once, and then again for each collection content change), and is showing up in profile traces as consuming between 2-5% of the total operation time. I am looking to see if anyone has an idea of how to speed this summation operation up, or if this is just the fastest its going to get.

Caching is not realistic in this case, because the amounts must be recalculated.

**EDIT For Ravadre and Joel - The total amount is stored at the class level (each amount collection and sum are contained in a class instance)

Any ideas?

A: 

That is about as fast as it is going to get given the fact that you are using a List(Of T).

Others have suggested that you use the Sum extension method - this will not improve performance as its implementation is identical to your current solution to the problem:

<Extension> _
Public Shared Function Sum(ByVal source As IEnumerable(Of Decimal)) As Decimal
    If (source Is Nothing) Then
        Throw Error.ArgumentNull("source")
    End If
    Dim num As Decimal = 0
    Dim num2 As Decimal
    For Each num2 In source
        num = (num + num2)
    Next
    Return num
End Function
Andrew Hare
The example is simplified, its actually a Dictionary(Of Integer, Decimal) and the Decimals are getting summed. Would that make a difference? Why?
StingyJack
+1  A: 

What about totalAmount = amountCollection.Sum() ?

Mitch Wheat
Checking on that now to see if a) its faster, and b) what the IL differences are. Thanks!
StingyJack
-1 This doesn't improve performance as the implementation of `Sum` is almost _identical_ to what the OP has already written. Please see my answer.
Andrew Hare
Regardless of whether it is faster, using Sum() is cleaner code.
Mitch Wheat
As Andrew said, Enumerable.Sum() does the same thing. In fact, practically every Enumerable extension method does *exactly* the same thing.
Rex M
@Mitch - I agree that `Sum()` is cleaner code, unfortunately that has nothing to do with the question.
Andrew Hare
A: 

Your other alternative is using for loop, you wouldn't have to create an enumerator, but practically there's no significant boost anyway (as far as I know).

Why you can't cache the result? If you cache total ammount, and for each change of one of your elements, modify amount using the same value (and eventually compute all each 10 modifications for synch). So when one of the elements is changed from 2.5 to 3.6, you just add 1.1 to your total amount. Why this won't work in your case?

Ravadre
updated OP with more info
StingyJack
I don't see a problem to be honest. you could wrap your list, so that you raise an event each time you try to modify something, or just raise it every time you catch, that someone is modifying it, then register to this event (where it's convenient to you) and modify this total amount. This is just one explanation, or I don't (yet) see the whole problem?
Ravadre
Oh, and by the way, if it's possible, try using doubles instead of decimals (may be not possible for what you are doing), they are significantly faster.
Ravadre
That's more or less what I understood from statenjason's idea. Thanks
StingyJack
RE: Doubles.... see comment to mquander's post.
StingyJack
StingyJack - yup, So no doubles for you, and +/- solution I was talking about, you can also check this: http://msdn.microsoft.com/en-us/library/ms668604.aspx .
Ravadre
+2  A: 

It's not clear to me how you expect to add up a list of numbers faster than adding each one in turn!

However, using decimals to do arithmetic is probably much, much, much slower than using a floating-point type, so if you don't actually need the precision, I suggest switching.

mquander
its currency amounts, so about 5 places is all I need, but the incoming values and the comparison values are all decimals, so there is a probably a significant boxing penalty to using float just for this implementation.
StingyJack
@StingyJack - Please don't even consider changing decimal to a binary floating point number if you're dealing with currency!
Greg Beech
@StingyJack - what 'significant boxing penalty' ??
Marc
Marc, I mean calling Convert.ToFloat() or CDec() several hundred times in a total operation. Even if they are mostly a 1:1 convert, thats still an extra step that isnt needed, and the object is to make it faster.
StingyJack
Greg... I have only ever used decimal, but what the deal with using Floats?
StingyJack
A: 

there are at least 50 individual amount collections that need to be summed in a total operation.

What about caching the sums from each individual collection? Then when a collection changes you only have to sum up that collection and the cached totals.

Joel Coehoorn
updated OP with more info
StingyJack
Thats basically what I am doing.
StingyJack
+3  A: 

Try subclassing List(Of Decimal), expose a TotalSum property. Each time Add() is called, increment by the insterted value. That'll keep you from having to iterate through the list to get the sum.

Edit: After trying, it would be better to implement IList(Of Decimal) because List's methods not being virtual. Something along the lines of this (Yes, I know it's C#...)

public class DecimalSumList : IList<decimal>
{
    readonly IList<decimal> _allValues = new List<decimal>();
    decimal _totalSum;
    public decimal TotalSum { get { return _totalSum; } }
    public void Add(decimal item)
    {
        _totalSum += item;
        _allValues.Add(item);
    }

    public void CopyTo(decimal[] array, int arrayIndex)
    {
        _allValues.CopyTo(array, arrayIndex);
    }

    bool ICollection<decimal>.Remove(decimal item)
    {
        _totalSum -= item;
        return _allValues.Remove(item);
    }

    public int Count
    {
        get { return _allValues.Count; }
    }

    public bool IsReadOnly
    {
        get { throw new NotImplementedException(); }
    }

    public void Remove(decimal item)
    {
        _totalSum -= item;
        _allValues.Remove(item);
    }
    public void Clear()
    {
        _totalSum = 0;
        _allValues.Clear();
    }

    public bool Contains(decimal item)
    {
        return _allValues.Contains(item);
    }

    public IEnumerator<decimal> GetEnumerator()
    {
        return _allValues.GetEnumerator();
    }

    IEnumerator IEnumerable.GetEnumerator()
    {
        return GetEnumerator();
    }

    public int IndexOf(decimal item)
    {
        return _allValues.IndexOf(item);
    }

    public void Insert(int index, decimal item)
    {
        _totalSum += item;
        _allValues.Insert(index,item);
    }

    public void RemoveAt(int index)
    {
        _totalSum -= _allValues[index];
        _allValues.RemoveAt(index);
    }

    public decimal this[int index]
    {
        get { return _allValues[index]; }
        set { _allValues[index] = value; }
    }
}
statenjason
Implemented something similar along these lines, and did see some improvement. Thanks!
StingyJack
+1  A: 

2-5% of your total profile trace time just isn't that much.

Even if you cut the execution time of this method in half, you will have saved at most a few percent of your total runtime.

Your best bet is probably to look elsewhere first.

StarPacker
Winning the performance war 5% at a time... http://blogs.msdn.com/ricom/archive/2005/10/17/481999.aspx
StingyJack
A: 

If you don't want to implement a version of IList/IDictionary as statenjason suggested you could create a wrapper :

Public Class TotalDictionaryWrapper
        Private innerDictionary As IDictionary(Of Integer, Decimal)
        Private m_total As Decimal = 0D
        
        Public Sub New(ByVal dictionary As IDictionary(Of Integer, Decimal))
            Me.innerDictionary = dictionary
        End Sub
        
        Public ReadOnly Property Total() As [Decimal]
            Get
                Return Me.m_total
            End Get
        End Property
        
        Public Sub Add(ByVal key As Integer, ByVal value As Decimal)
            Me.innerDictionary.Add(key, value)
            m_total += value
        End Sub
        
        Public Sub Remove(ByVal key As String)
            Dim toRemove As Decimal = Me.innerDictionary(key)
            Me.innerDictionary.Remove(key)
            Me.m_total -= toRemove
        End Sub
        
        Public Sub Update(ByVal key As Integer, ByVal newValue As Decimal)
            Dim oldValue As Decimal = Me.innerDictionary(key)
            Me.innerDictionary(key) = newValue
            Me.m_total -= oldValue
            Me.m_total += newValue
        End Sub
        
        Other methods..

    End Class
Gary W
+2  A: 

If these are currency values, consider using scaled integers. For 2 decimal places, you store the # of pennies instead of the number of dollars, then divide by 100 when you need to output the result. For 5 decimal places, your variables internally use the original number * 100000. This should improve both performance and accuracy, especially if you're doing any division.

Beth
We did toss that around as an idea, and my cohort is trying it out to simplify the division / rounding. <br />In the example I have above, that adds at least 3 multiplications (to convert the added decimals to ints) and a division (to convert the result back to a decimal). I dont think that would be an improvement in speed in this case, but we are trying it.
StingyJack
No, the point isn't to convert them back and forth, it's to store them as scaled integers the whole way through and never touch decimals!
mquander
mquander's got it- you store as, let's say, whole # of pennies. during your calculations, you round to the nearest penny if you need to divide. when reporting, you divide by 100, again rounding to the nearest penny.
Beth