40

I really like Last() and would use it all the time for List<T>s. But since it seems to be defined for IEnumerable<T>, I guess it enumerates the enumeration first - this should be O(n) as opposed to O(1) for directly indexing the last element of a List<T>.

Are the standard (Linq) extension methods aware of this?

The STL in C++ is aware of this by virtue of a whole "inheritance tree" for iterators and whatnot.

Pavel Chuchuva
  • 22,633
  • 10
  • 99
  • 115
Daren Thomas
  • 67,947
  • 40
  • 154
  • 200
  • What inheritance tree for iterators? C++ doesn't have extension methods in the first place, so `end()` on `vector` is simply implemented differently from that of `list`, and anything that wants to work with iterators for either one has to be a `template` on `Iterator` type parameter. – Pavel Minaev Sep 04 '09 at 08:19

5 Answers5

56

I just used the Reference Source to look into the code for Last and it checks to see if it is a IList<T> first and performs the appropriate O(1) call:

public static TSource Last < TSource > (this IEnumerable < TSource > source) {
    if (source == null) throw Error.ArgumentNull("source");
    IList < TSource > list = source as IList < TSource > ;
    if (list != null) {
        int count = list.Count;
        if (count > 0) return list[count - 1];
    }
    else {
        using(IEnumerator < TSource > e = source.GetEnumerator()) {
            if (e.MoveNext()) {
                TSource result;
                do {
                    result = e.Current;
                } while ( e . MoveNext ());
                return result;
            }
        }
    }
    throw Error.NoElements();
}

So you have the slight overhead of a cast, but not the huge overhead of enumerating.

HarshWombat
  • 121
  • 1
  • 7
Martin Harris
  • 28,277
  • 7
  • 90
  • 101
  • One issue which was highlighted (and now the comments have been deleted making me look like a raving idiot) is that anybody working on an implementation of the framework - like Mono - can not have ever seen any code that Microsoft wrote so at the very least it is rude to post it in the clear. I'm not having much luck actually searching to see it is is actually *illegal* and I'd be interested to see a cite that it is. See the rules here http://www.mono-project.com/Contributing – Martin Harris Sep 04 '09 at 08:34
  • 13
    It's probably also illegal to post a picture of the wheels of my chair, because somebody invited it. Sometimes I think people just get crazy about copyright issues. – Stefan Steinegger Sep 04 '09 at 09:25
  • I posted a question on meta here (http://meta.stackexchange.com/questions/20153/posting-code-from-reflector) which pretty much convinced me that is *is* illegal. As I mentioned though I only deleted it because some comments (which have now also been deleted) suggested I did. I have posted code from reflector before and I'm sure I will again. I'm not looking over my shoulder for the Microsoft copyright SWAT team since I would argue that anything to improve the understanding and trust in .NET benefits Microsoft anyway. – Martin Harris Sep 04 '09 at 09:33
  • 1
    "if (this.m_handle == IntPtr.Zero)" (soundtrack to this comment: Judas Priest - Breakin' The Law) – mackenir Sep 04 '09 at 10:13
  • How about just looking at the IL and writing equivalent C# code from out of your own head? Is it illegal to look at IL, or to translate IL into another language and then write that down? – mackenir Sep 04 '09 at 10:15
  • The Mono project doesn't let contributors look at the IL either, and I'm sure they've done more research on this than I have so I'd assume that they see some risk in it. I guess even if you do it by hand it is still disassembly and breaks the EULA. – Martin Harris Sep 04 '09 at 10:30
  • 3
    Posting snippets from Reflector is not an issue. – user7116 Sep 04 '09 at 10:34
  • 4
    On the plus side, the code is still in the Edit history :D Down with lawyers. – Gusdor Dec 04 '13 at 12:22
  • Ha, it's open source now, bet your lawyer didn't see that coming. – juharr Apr 12 '17 at 15:13
  • 1
    You can use the [Reference Source](https://referencesource.microsoft.com/#System.Core/System/Linq/Enumerable.cs,3628defc5be1468a) now and see the implementation of the Last extension for yourself. – Matthiee Jul 30 '17 at 09:59
  • Is [^1] considered faster or not? – juFo Aug 08 '22 at 08:02
23

You can just use Last with List<T> without worrying :)

Enumerable.Last attempts to downcast the IEnumerable<T> instance to IList<T> . If this is possible, it uses the indexer and Count property.

Here is part of the implementation as Reflector sees it:

IList<TSource> list = source as IList<TSource>;
if (list != null)
{
    int count = list.Count;
    if (count > 0)
    {
        return list[count - 1];
    }
}
Mark Seemann
  • 225,310
  • 48
  • 427
  • 736
9

It contains an optimisation for anything that implements IList<T> in which case it just looks up the item at length -1.

Keep in mind that the vast majority of stuff you will send in will implement IList<T>

List<int> 
int[] 

and so on ... all implement IList<T>

For those who can not look at the code to confirm, you can confirm it using observation:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;

namespace ConsoleApplication4 {
    class Program {

        static void Profile(string description, int iterations, Action func) {

            // clean up
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            // warm up 
            func();

            var watch = Stopwatch.StartNew();
            for (int i = 0; i < iterations; i++) {
                func();
            }
            watch.Stop();
            Console.Write(description);
            Console.WriteLine(" Time Elapsed {0} ms", watch.ElapsedMilliseconds);
        }

        static void Main(string[] args) {
            int[] nums = Enumerable.Range(1, 1000000).ToArray();

            int a;

            Profile("Raw performance", 100000, () => { a = nums[nums.Length - 1];  });
            Profile("With Last", 100000, () => { a = nums.Last(); }); 

            Console.ReadKey();
        }


    }
}

Output:

Raw performance Time Elapsed 1 ms
With Last Time Elapsed 31 ms

So it's only 30 times slower and maintains that performance profile with whatever length list you have, which is nothing in the big scheme of things.

Sam Saffron
  • 128,308
  • 78
  • 326
  • 506
2

For List<T> it is O(1), but for other enumerables it may be O(N).

Brian Rasmussen
  • 114,645
  • 34
  • 221
  • 317
0

Short answer:

O(1).

Explanation:

It's evident that Last() for List uses Count() extension method.

Count() checks type of the collection in runtime and uses Count property if it's available.

Count property for list has O(1) complexity so is the Last() extension method.

Konstantin Spirin
  • 20,609
  • 15
  • 72
  • 90
  • `Last` *does not* use the `Count` extension method, but you're right that both methods can be O(1) in certain circumstances: `Last` checks if the collection implements `IList` and if so just gets the last element by index rather than enumerating; The `Count` method checks if the collection implements `ICollection` and if so just gets the `Count` property rather than enumerating. – LukeH Sep 05 '09 at 01:07
  • Thank Luke, I was not 100% right. Last uses property Count for IList and has complexity of O(1). I just checked out code for Last(this collection, predicate) and it's O(N). That is lazy. – Konstantin Spirin Sep 05 '09 at 03:12
  • @Konstantin: If you pass a predicate to the `Last` method then it has to do an O(n) enumeration of the collection to determine which items match the predicate. – LukeH Sep 05 '09 at 23:24
  • Luke, current implementation _always_ cycles throught the whole collection whereas what I propose gives O(n) in worst case and O(1) if number of matching elements is O(n). – Konstantin Spirin Sep 07 '09 at 05:38
  • 1
    @Konstantin: I'm not sure exactly what you are proposing. Do you mean iterating backwards through the `IList` until you find the first (ie, last) item that matches the predicate? That would improve the best-case performance over what's currently in the framework. – LukeH Sep 07 '09 at 11:08