Przykład:
{
int[] a1 = new int[] { 145, 2, 3 };
int[] a2 = new int[] { 145, 2, 3 };
object[] a3 = new object[] { 145, 2, 3 };
object[] a4 = new object[] { 145, 2, 3 };
Debug.Assert((a1[0].Equals(a2[0])));
Debug.Assert(a1[0] == a2[0]);
Debug.Assert((a3[0].Equals(a4[0])));
Debug.Assert(a3[0] != a4[0]);
Debug.Assert((a1[0].Equals(a3[0])));
//Debug.Assert(a1[0] == a3[0]);
Debug.Assert((a3[0].Equals(a1[0])));
//Debug.Assert(a3[0] == a1[0]);
Debug.Assert(a1 != a2);
Debug.Assert(!a1.Equals(a2));
Debug.Assert(!Array.Equals(a1, a2));
Debug.Assert(a3 != a4);
Debug.Assert(!a3.Equals(a4));
Debug.Assert(!Array.Equals(a3, a4));
//Debug.Assert(a1 == a3);
Debug.Assert(!a1.Equals(a3));
Debug.Assert(!Array.Equals(a1, a3));
//Debug.Assert(a3 == a1);
Debug.Assert(!a3.Equals(a1));
Debug.Assert(!Array.Equals(a3, a1));
Debug.Assert(a1.SequenceEqual(a2));
Debug.Assert(a3.SequenceEqual(a4));
//Debug.Assert(a1.SequenceEqual(a3));
//Debug.Assert(a3.SequenceEqual(a1));
}
Linie zakomentowane nie kompilują się. Jak widać obiekt typu
Array
, po którym dziedziczą wszystkie tablice, jak i te tablice nie przeładowują operatora ==, ani metody
Equals()
, dzięki temu wszystkie porównania tablic sprowadzają się do porównania referencji do nich. Sam obiekt
Array
nie udostępnia żadnej metody, która by nam w takim porównaniu pomogła. Więcej, samo środowisko czegoś takiego nam nie udostępnia, poza metodą
SequenceEqual
dostarczoną przez LINQ. Musimy więc sami zająć się sprawą porównywania tablic. Poniższy kod prezentuje 6 różnych metod porównywania tablic dla typów wartościowych i 5 dla typów referencyjnych. W metodzie
Test1()
porównywane są typy wartościowe. W metodzie
Test2
porównywane są typy referencyjne. Dla każdej metody testowej najpierw sprawdzana jest poprawność algorytmów porównujących. A później wykonywane są dwie serie pomiarów. Najpierw duża tablica i mało sprawdzeń, później mała tablica i dużo sprawdzeń. Ilość porównań elementów tablicy jest taka sama w obu przypadkach. Chodzi tutaj o sprawdzenie kosztu samego porównania tablic.
public void Measure(int repeats, Action a)
{
Stopwatch sw = new Stopwatch();
sw.Start();
for (int i = 0; i < repeats; i++)
a();
sw.Stop();
System.Console.WriteLine(sw.ElapsedMilliseconds);
}
public bool Compare1<T>(T[] ar1, T[] ar2)
{
if (ar1.Length != ar2.Length)
return false;
for (int i = 0; i < ar1.Length; i++)
if (!ar1[i].Equals(ar2[i]))
return false;
return true;
}
public bool Compare2<T>(T[] ar1, T[] ar2, Func<T, T, bool> comparer)
{
if (ar1.Length != ar2.Length)
return false;
for (int i = 0; i < ar1.Length; i++)
if (!comparer(ar1[i], ar2[i]))
return false;
return true;
}
public bool Compare3<T>(T[] ar1, T[] ar2, IEqualityComparer<T> comparer)
{
if (ar1.Length != ar2.Length)
return false;
for (int i = 0; i < ar1.Length; i++)
if (!comparer.Equals(ar1[i], ar2[i]))
return false;
return true;
}
public bool Compare4a(int[] ar1, int[] ar2)
{
if (ar1.Length != ar2.Length)
return false;
for (int i = 0; i < ar1.Length; i++)
if (ar1[i] != ar2[i])
return false;
return true;
}
public bool Compare4b(object[] ar1, object[] ar2)
{
if (ar1.Length != ar2.Length)
return false;
for (int i = 0; i < ar1.Length; i++)
if (!ar1[i].Equals(ar2[i]))
return false;
return true;
}
class Comparer1 : IEqualityComparer<int>
{
public bool Equals(int x, int y)
{
return x == y;
}
public int GetHashCode(int obj)
{
throw new NotImplementedException();
}
}
class Comparer2 : IEqualityComparer<object>
{
public bool Equals(object x, object y)
{
return x.Equals(y);
}
public int GetHashCode(object obj)
{
throw new NotImplementedException();
}
}
public void Test1()
{
int[] ar1 = Enumerable.Range(1, 1000000).ToArray();
int[] ar2 = Enumerable.Range(1, 1000000).ToArray();
int[] ar3 = Enumerable.Range(2, 1000000).ToArray();
int[] ar4 = Enumerable.Range(1, 1000000 - 1).ToArray();
Debug.Assert(Compare1(ar1, ar2));
Debug.Assert(Compare1(ar1, ar1));
Debug.Assert(!Compare1(ar1, ar3));
Debug.Assert(!Compare1(ar1, ar4));
Debug.Assert(Compare2(ar1, ar2, (a, b) => (a == b)));
Debug.Assert(Compare2(ar1, ar1, (a, b) => (a == b)));
Debug.Assert(!Compare2(ar1, ar3, (a, b) => (a == b)));
Debug.Assert(!Compare2(ar1, ar4, (a, b) => (a == b)));
Debug.Assert(Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
Debug.Assert(Compare2(ar1, ar1, (a, b) => (a.Equals(b))));
Debug.Assert(!Compare2(ar1, ar3, (a, b) => (a.Equals(b))));
Debug.Assert(!Compare2(ar1, ar4, (a, b) => (a.Equals(b))));
Debug.Assert(Compare3(ar1, ar2, new Comparer1()));
Debug.Assert(Compare3(ar1, ar1, new Comparer1()));
Debug.Assert(!Compare3(ar1, ar3, new Comparer1()));
Debug.Assert(!Compare3(ar1, ar4, new Comparer1()));
Debug.Assert(Compare4a(ar1, ar2));
Debug.Assert(Compare4a(ar1, ar1));
Debug.Assert(!Compare4a(ar1, ar3));
Debug.Assert(!Compare4a(ar1, ar4));
Measure(100, () => ar1.SequenceEqual(ar2));
Measure(100, () => Compare1(ar1, ar2));
Measure(100, () => Compare2(ar1, ar2, (a, b) => (a == b)));
Measure(100, () => Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
Measure(100, () => Compare3(ar1, ar2, new Comparer1()));
Measure(100, () => Compare4a(ar1, ar2));
System.Console.WriteLine("----");
ar1 = Enumerable.Range(1, 1000000 / 100000).ToArray();
ar2 = Enumerable.Range(1, 1000000 / 100000).ToArray();
Measure(100 * 100000, () => ar1.SequenceEqual(ar2));
Measure(100 * 100000, () => Compare1(ar1, ar2));
Measure(100 * 100000, () => Compare2(ar1, ar2, (a, b) => (a == b)));
Measure(100 * 100000, () => Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
Measure(100 * 100000, () => Compare3(ar1, ar2, new Comparer1()));
Measure(100 * 100000, () => Compare4a(ar1, ar2));
System.Console.WriteLine("----");
}
void Test2()
{
object[] ar1 = (from n in Enumerable.Range(1, 1000000) select (object)n).ToArray();
object[] ar2 = (from n in Enumerable.Range(1, 1000000) select (object)n).ToArray();
object[] ar3 = (from n in Enumerable.Range(2, 1000000) select (object)n).ToArray();
object[] ar4 = (from n in Enumerable.Range(1, 1000000-1) select (object)n).ToArray();
Debug.Assert(Compare1(ar1, ar2));
Debug.Assert(Compare1(ar1, ar1));
Debug.Assert(!Compare1(ar1, ar3));
Debug.Assert(!Compare1(ar1, ar4));
Debug.Assert(Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
Debug.Assert(Compare2(ar1, ar1, (a, b) => (a.Equals(b))));
Debug.Assert(!Compare2(ar1, ar3, (a, b) => (a.Equals(b))));
Debug.Assert(!Compare2(ar1, ar4, (a, b) => (a.Equals(b))));
Debug.Assert(Compare3(ar1, ar2, new Comparer2()));
Debug.Assert(Compare3(ar1, ar1, new Comparer2()));
Debug.Assert(!Compare3(ar1, ar3, new Comparer2()));
Debug.Assert(!Compare3(ar1, ar4, new Comparer2()));
Debug.Assert(Compare4b(ar1, ar2));
Debug.Assert(Compare4b(ar1, ar1));
Debug.Assert(!Compare4b(ar1, ar3));
Debug.Assert(!Compare4b(ar1, ar4));
Measure(100, () => ar1.SequenceEqual(ar2));
Measure(100, () => Compare1(ar1, ar2));
Measure(100, () => Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
Measure(100, () => Compare3(ar1, ar2, new Comparer2()));
Measure(100, () => Compare4b(ar1, ar2));
System.Console.WriteLine("----");
ar1 = (from n in Enumerable.Range(1, 1000000 / 100000) select (object)n).ToArray();
ar2 = (from n in Enumerable.Range(1, 1000000 / 100000) select (object)n).ToArray();
Measure(100 * 100000, () => ar1.SequenceEqual(ar2));
Measure(100 * 100000, () => Compare1(ar1, ar2));
Measure(100 * 100000, () => Compare2(ar1, ar2, (a, b) => (a.Equals(b))));
Measure(100 * 100000, () => Compare3(ar1, ar2, new Comparer2()));
Measure(100 * 100000, () => Compare4b(ar1, ar2));
System.Console.WriteLine("----");
}
public void Test()
{
Test1();
Test2();
}
Wyniki:
2317 1891 1338 1578 1307 596 ---- 3708 1957 1531 1740 1552 730 ---- 3293 1193 2059 2300 1192 ---- 4972 1261 2151 2448 1246 ----
Najpierw może jeśli chodzi o sam koszt uruchomienia porównania tablic. Widać, że milion razy więcej wywołań przekłada się na niewielki wzrost miliona porównań. Czyli czas samego wywołania porównania jest mały. Czas ten wzrasta znacznie dla
SequenceEqual()
. Jeśli chodzi o czas samego porównania to jak można się było spodziewać najwolniejsza jest metoda
SequenceEqual()
. Dla typów wartościowych najszybsze jest porównanie bezpośrednie tablic. Wolniejsze jest wykorzystanie
IEqualityComparer
. Prawie tak samo szybkie jest użycie jako metody porównawczej wyrażenia lambda z bezpośrednim porównaniem wartości. Dalej jest porównanie z użyciem wyrażenia lambda, ale wykorzystujące metodę
Equals()
. Dalej metoda
Compare1()
, która także wykorzystuje
Equals()
do porównania. Najwolniejsza jest wspomniana
SequenceEqual()
. Dla typów referencyjnych jest trochę inaczej. Nie ma tutaj sensu osobno porównywać za pomocą operatora == i
Equals()
stąd sposobów porównania jest o jeden mniej. Najszybciej działa bezpośrednie porównanie tablic. Dalej jest metoda
Compare1
wykorzystująca
Equals()
. Dalej jest wykorzystanie wyrażenia lambda, które wykorzystuje
Equals()
. Dalej wykorzystanie
IEqualityComparer
. Na końcu wykorzystanie
SequenceEqual()
. Jeśli chodzi o metody, które nadają się zarówno do porównania typów wartościowych, jak i referencyjnych to w grę wchodzi tylko wykorzystanie
SequenceEqual()
lub
Compare1
.
Compare1
jest szybsze, ale za to
SequenceEqual()
bardziej uniwersalne (działa na enumeratorach) i dostępne w standardzie. Jeśli chodzi o prędkość to niestety czym coś jest bardziej uniwersalne to tym jest wolniejsze. Skupmy się na metodzie
Compare1()
. Napiszmy ją tak by stała się uniwersalną metodą rozszerzoną:
public static class ArrayExtensions
{
public static bool SameArrays<T>(this T[] ar1, T[] ar2)
{
if (Object.ReferenceEquals(ar1, ar2))
return true;
if (ar1.Length != ar2.Length)
return false;
for (int i = 0; i < ar1.Length; i++)
if (!ar1[i].Equals(ar2[i]))
return false;
return true;
}
}
public void Test3()
{
{
int[] ar1 = Enumerable.Range(1, 1000000).ToArray();
int[] ar2 = Enumerable.Range(1, 1000000).ToArray();
int[] ar3 = Enumerable.Range(2, 1000000).ToArray();
int[] ar4 = Enumerable.Range(1, 1000000 - 1).ToArray();
Debug.Assert(ar1.SameArrays(ar2));
Debug.Assert(ar1.SameArrays(ar1));
Debug.Assert(!ar1.SameArrays(ar3));
Debug.Assert(!ar1.SameArrays(ar4));
}
{
object[] ar1 = (from n in Enumerable.Range(1, 1000000) select (object)n).ToArray();
object[] ar2 = (from n in Enumerable.Range(1, 1000000) select (object)n).ToArray();
object[] ar3 = (from n in Enumerable.Range(2, 1000000) select (object)n).ToArray();
object[] ar4 = (from n in Enumerable.Range(1, 1000000 - 1) select (object)n).ToArray();
Debug.Assert(ar1.SameArrays(ar2));
Debug.Assert(ar1.SameArrays(ar1));
Debug.Assert(!ar1.SameArrays(ar3));
Debug.Assert(!ar1.SameArrays(ar4));
}
}
Dodatkowo, dodałem tam drobną optymalizację, by nie porównywać tych samych obiektów.
Brak komentarzy:
Prześlij komentarz