Przetestowałem pierwszą wersję programu i jest świetny.
Niestety linki do nowszych wersji są nieaktywne.
Może ktoś udostępnić najnowszą wersję? A może sam autor?
Przetestowałem pierwszą wersję programu i jest świetny.
Niestety linki do nowszych wersji są nieaktywne.
Może ktoś udostępnić najnowszą wersję? A może sam autor?
Dawno do tego programiku nie zaglądałem :P. W każdym bądź razie … wrzucam link, który nie wygaśnie Duplicate Finder v1.2. Jakbyś znalazł jakieś błędy, coś co Twoim zdaniem można zmienić, to zgłaszaj proszę tutaj w temacie albo przez priv-a.
Zdaje się, że miałem coś tu napisać już dawno temu… Dobrze, że ktoś to odkopał.
Tylko raz brałem udział w pisaniu większego systemu, w którym potrzebna była funkcjonalność usuwania znaków diakrytycznych z tekstu. Akurat ja ją pisałem i zrealizowałem za pomocą zwykłego słownika. Owszem, jest to rozwiązanie nieelastyczne, ale i tak, gdyby przypadkiem system ten miał mieć innojęzyczną wersję, to i tak trzeba ją na nowo skompilować i wdrożyć u klienta. (To nie było coś do używania na jednym domowym pececie. ;))
Mógłbyś zdradzić jak to realizujesz?
Widzę, że w cała aplikacja się bardzo rozrosła, chyba jeszcze tylko kawy nie parzy.
A zaległe “inne spojrzenie na ten problem” opiszę teraz.
Zasady, na jakich mamy porównywać wiersze znamy jeszcze przed rozpoczęciem procesu. Dlatego od razu widać, że sprawdzanie wszystkich warunków logicznych opisujących te zasady dla każdego wiersza osobno jest bez sensu. To tylko niepotrzebny nakład pracy procesora mający negatywne przełożenie na efektywność przetwarzania. Z tego powodu trzeba wymyślić mechanizm, dzięki któremu będzie możliwe jednokrotne skonfigurowanie algorytmu realizującego warunki przetwarzania, a potem użycie go dla każdego wiersza, tym razem bez ponownego sprawdzania warunków.
Ogólna koncepcja jest taka, że potrzebujemy głównej klasy z jakąś metodą, w której wczytamy plik, będziemy przetwarzali wiersze po kolei, a potem zapiszemy plik wynikowy. Przy okazji zbierzemy różne informacje o wyniku działania programu, które potem wyświetlimy użytkownikowi. Samo przetwarzanie wierszy, zgodnie z zasadą SRP, powinien realizować obiekt innej klasy. Jak już wiemy, wiersze mogą być przetwarzane na kilka sposobów (przy czym w przyszłości mogą pojawić się kolejne). Potrzebujemy zatem zestawu klas z metodami do przetwarzania wierszy (jedna klasa z metodą dla jednego sposobu). Ponadto konieczne jest umożliwienie pętli z głównej klasy programu, użycie dowolnego sposobu, czyli współpracy z dowolną z klas zawierających algorytm przetwarzania.
Zaczniemy zatem od takiego interfejsu:
interface IRowTransformingStrategy
{
string Transform(string row);
}
Zatem najprostszy w implementacji transformer wiersza będzie wyglądał tak:
class SimpleTransformer : IRowTransformingStrategy
{
public string Transform(string row)
{
return row;
}
}
A transformer ignorujący białe znaki np. tak:
class IgnoreWhitespaceTransformer : IRowTransformingStrategy
{
private static readonly Regex regex = new Regex(@"\s+", RegexOptions.Compiled);
public override string Transform(string row)
{
return regex.Replace(this.transformer.Transform(row), string.Empty);
}
}
Jak można się domyślić, możemy analogicznie stworzyć miliony klas transformujących, ale na razie to zostawimy. W zasadzie sam interfejs wystarczy nam, żeby napisać już główną klasę programu:
class DuplicatesRemover
{
public string InputFilePath { get; private set; }
public string OutputFilePath { get; private set; }
private IRowTransformingStrategy rowStrategy;
public DuplicatesRemover(string inputFilePath, IRowTransformingStrategy rowStrategy)
{
this.InputFilePath = inputFilePath;
this.OutputFilePath = Path.Combine(Path.GetDirectoryName(inputFilePath),
string.Format("{0}_result{1}", Path.GetFileNameWithoutExtension(inputFilePath), Path.GetExtension(inputFilePath)));
this.rowStrategy = rowStrategy;
}
public RemovalResult Process()
{
HashSet tempHash = new HashSet();
List outputRows = new List();
Stopwatch sw = new Stopwatch();
sw.Start();
string[] inputRows = File.ReadAllLines(this.InputFilePath);
for (int i = 0; i < inputRows.Length; i++)
{
if (!string.IsNullOrEmpty(inputRows[i]))
{
string ts = this.rowStrategy.Transform(inputRows[i]);
if (tempHash.Add(ts))
{
outputRows.Add(inputRows[i]);
}
}
}
File.WriteAllLines(this.OutputFilePath, outputRows);
sw.Stop();
return new RemovalResult(this.OutputFilePath, sw.Elapsed, inputRows.Length, outputRows.Count);
}
}
W polu rowStrategy siedzi referencja do obiektu implementującego metodę z odpowiednim algorytmem przetwarzania wiersza tekstu. Obiekt ten przekazujemy w konstruktorze. W ten sposób możemy podać w czasie korzystania z programu dowolny algorytm, nie mamy go na sztywno zaszytego w kodzie. Ponadto, gdy będą powstawały nowe algorytmy porównywania wierszy, nie będziemu musieli nic zmieniać w tej klasie, bo nie ona zajmuje sę przetwarzaniem wiersza. Wygląda dobrze. Ta koncepcja jest tak prosta, że ma nawet swoją nazwę: http://pl.wikipedia.org/wiki/Strategia_ … jektowy%29Gwoli ścisłości, metoda Process zwraca obiekt takiej prostej klasy:
class RemovalResult
{
public string OutputFilePath { get; private set; }
public TimeSpan Duration { get; private set; }
public int TotalRowsCount { get; private set; }
public int DistinctRowsCount { get; private set; }
public int RemovedRowsCount { get; private set; }
public RemovalResult(string outputFilePath, TimeSpan duration, int totalRowsCount, int distinctRowsCount)
{
this.OutputFilePath = outputFilePath;
this.Duration = duration;
this.TotalRowsCount = totalRowsCount;
this.DistinctRowsCount = distinctRowsCount;
this.RemovedRowsCount = totalRowsCount - distinctRowsCount;
}
public override string ToString()
{
return string.Format("Czas operacji: {0}\nLiczba wierszy: {1}\nUsunięto: {2}\nPozostawiono: {3}\nPlik wyjściowy: {4}",
this.Duration, this.TotalRowsCount, this.RemovedRowsCount, this.DistinctRowsCount, this.OutputFilePath);
}
}
No dobra, zatem za pomocą tego wszystkiego możemy już najpierw utworzyć obiekt z algorytmem, a potem przekazać go do obiektu głównej klasy i przetworzyć nim wszystkie wiersze. Nie trzeba już za każdym razem w pętli sprawdzać warunków. Super! Ale to nie wszystko… Użytkownik może zechcieć jednocześnie ignorować w wierszu np. zarówno cyfry, jak i białe znaki. Co wtedy? Przecież nie będziemy pisali oddzielnej klasy strategii dla każdej możliwej kombinacji dowolnego podzioru sposobów przetwarzania wierszy. Byłoby ich stanowczo zbyt wiele. Trzeba zatem wymyślić, jak połączyć ze sobą obiekty klas transformujących dynamicznie, w sposób pozwalający na wykonanie szeregu transformacji na tym samym wierszu tekstu. Rozwiązanie nie jest trudne - niech każda klasa przetwarzająca posiada pole typu strategii. W momencie tworzenia obiektu tej klasy przekażemy mu w konstruktorze obiekt, który ustawimy w tym polu. Teraz musimy już tylko pamiętać, żeby metoda Transform nie operowała bezpośrednio na swoim argumencie, ale najpierw wywołała na nim metodę Transform swojego wewnętrznego transformera. W sumie sam niewiele rozumiem z tego zaciemnionego opisu, więc lepiej pokażę kod. Nowa klasa bazowa dla naszych klas przetwarzających:
abstract class ExtendedTransformerBase : IRowTransformingStrategy
{
protected IRowTransformingStrategy transformer;
public ExtendedTransformerBase()
{
this.transformer = new SimpleTransformer();
}
public ExtendedTransformerBase(IRowTransformingStrategy transformer)
{
this.transformer = transformer;
}
public abstract string Transform(string row);
}
Jak widać, ma wewnętrzne pole strategii, w którym w konstruktrze umieścimy obiekt, który opakowujemy. Jeśli wywołamy konstruktor bezparametrowy to znajdzie się w nim obiekty typu SimpleTransformer (ta klasa pozostaje bez zmian). Za to kolejne klasy przetwarzające nie będą po prostu implementowały interfejsu, tylko dziedziczyły po nowej klasie, np.:
class IgnoreWhitespaceTransformer : ExtendedTransformerBase
{
public IgnoreWhitespaceTransformer() : base() { }
public IgnoreWhitespaceTransformer(IRowTransformingStrategy transformer) : base(transformer) { }
private static readonly Regex regex = new Regex(@"\s+", RegexOptions.Compiled);
public override string Transform(string row)
{
return regex.Replace(this.transformer.Transform(row), string.Empty);
}
}
class IgnoreCaseTransformer : ExtendedTransformerBase
{
public IgnoreCaseTransformer() : base() { }
public IgnoreCaseTransformer(IRowTransformingStrategy transformer) : base(transformer) { }
public override string Transform(string row)
{
return this.transformer.Transform(row).ToLower(CultureInfo.InvariantCulture);
}
}
Jak widać, metody Transform nie operują już bezpośrednio na argumencie row tylko najpierw przetwarzają go przez this.transformer.Transform(row). W ten sposób dynamicznie rozszerzamy funkcjonalność istniejących klas. Pokuszę się o stwierdzenie, że jest to pewna wariacja na temat tego wzorca: http://pl.wikipedia.org/wiki/Dekorator_ … jektowy%29Teraz potrzebujemy już tylko fabryki, która zbuduje (na podstawie wymogów użytkownika) odpowiedni obiekt przetwarzający wiersz:
class RowTransformerFactory
{
public static IRowTransformingStrategy Create(bool ignoreCase, bool ignoreWhiteSpace, bool ignoreDigits, bool ignorePunctuations, bool ignoreDiacritics)
{
IRowTransformingStrategy strategy = new SimpleTransformer();
if (ignoreCase)
{
strategy = new IgnoreCaseTransformer(strategy);
}
if (ignoreWhiteSpace)
{
strategy = new IgnoreWhitespaceTransformer(strategy);
}
if (ignoreDigits)
{
strategy = new IgnoreDigitsTransformer(strategy);
}
if (ignorePunctuations)
{
strategy = new IgnorePunctuationsTransformer(strategy);
}
if (ignoreDiacritics)
{
strategy = new IgnoreDiacristicsTransformer(strategy);
}
return strategy;
}
}
Użycie:
IRowTransformingStrategy strategy = RowTransformerFactory.Create(false, false, true, false, true);
DuplicatesRemover remover = new DuplicatesRemover(@"D:\TempDev\test.txt", strategy);
RemovalResult result = remover.Process();
Console.WriteLine(result);
Link do projektu: http://somekind.pl/_upload/dp/Duplicate … Edition.7z
No i to by było na tyle.
Eh, jasne, jak pliki zawierają tylko jeden język, to można użyć słownika (zdecydowanie najbezpieczniejsze rozwiązanie). Sęk w tym, że ten program nie miał zawierać takiego ograniczenia. Skąd taki pomysł? Dany plik txt może zawierać fragmenty tekstu w różnych językach. Dlatego też szukałem rozwiązania uniwersalnego. To rozwiązanie, które użyłem, zostało zaproponowane przez pracownika Microsoft-u. Link do tego rozwiązania podałem wyżej (kilka miesięcy temu ). Nie jest one idealne, ale jest tam też o tym mowa, tzn. jest napisane co może w nim stanowić problem.
Nie ukrywam, że tutaj mogą pojawić się jakieś błędy. Jeśli taki błąd zostanie znaleziony (tzn. jakiś znak diaktryczny nie będzie poprawnie rozpoznawany) przy przetwarzaniu jakiegoś pliku txt, to należy w pliku diactrics.xml (który dołączony jest wraz z aplikacją) wprowadzić odpowiedni wyjątek (np. dla języka polskiego wyjątkiem jest litera ł). Automatycznie (lub po ponownym uruchomieniu aplikacji - już nie pamiętam, czy cache-owałem ten słownik, czy nie) zostanie on uwzględniony przy kolejnym przetwarzaniu tego samego pliku txt (oraz każdego innego oczywiście), aby wyeliminować jakiekolwiek problemy.
Niby mógłbym wprowadzić edytor graficzny do tego celu (żeby nie trzeba było ręcznie edytować tego pliku xml), ale jakoś na razie mi się nie chciało, bo nikt specjalnie błędów nie zgłaszał (jak nikt tego programu nie używa, to i nikt błędów nie zgłasza :twisted: ). Choć w sumie … jeśli użytkownik miałby już coś wprowadzać ręcznie to może lepiej, żeby od razu wprowadził wszystkie znaki diaktryczne jakie obowiązują w jego języku? Oczywiście byłyby one przechowywane w pliku xml (tak jak do tej pory), żeby nie była potrzebna ponowna kompilacja aplikacji. Może nawet coś z tego wprowadzę w przyszłym tygodniu. Zresztą i tak miałem wprowadzić poprawki w tej aplikacji, bo zauważyłem, że część etykiet w okienku jest w języku angielskim, a część w języku polskim Nie wiem jak mogłem to wcześniej przeoczyć.
Szczerze to nie jest to żadna tajemnica, bo kod nie był nawet poddany obfuskacji. Rozwiązania, które w minimalnym stopniu obciąża pamięć, zacząłem szukać po tym jak odezwała się do mnie osoba (przez priva), która miała do przetworzenia pliki o wielkości od 700 MB w górę. Niestety to, które wykombinowałem, nie jest wystarczająco wydajne (choć zużycie pamięci jest utrzymywane na minimalnym poziomie). Nie ukrywam, że jestem tutaj bardzo ciekaw tego, jak można by to optymalnie rozwiązać.
Tak, bo doszło trochę zmian. Nie chciało mi się wszystkiego opisywać (i nadal mi się nie chce :P). Między innymi wrzuciłem sobie wzorzec fabryki, o którym piszesz w tym poście.
Dzięki, zawsze dobrze spojrzeć na inny punkt widzenia.
Ok, ale czy porównywałeś działanie tych dwóch programów - Swojego i mojego? Utwórz sobie plik tekstowy zawierający wyłącznie liczby od 0 do 1000000 i przetwórz go przy użyciu obu programów (zaznacz u mnie wszystkie checkbox-y). W moim programie trwa to o prawie sekundę krócej (testowane na Lenovo G570 (59-070902)) - jak będzie trzeba to oczywiście wrzucę zrzuty ekranu.
I jeszcze coś … Testując Twoją aplikację znalazłem u Ciebie drobne błędy.
dfg
dfg 123
to u Ciebie w programie dwie różne linie. Tymczasem przy tych warunkach jest to przecież dokładnie to samo.
Na wstępie, diaKRYTYczny, nie diaKTRYczny!
Moim zdaniem oba rozwiązania są równie dobre i złe, wszystko zależy. Chociaż ja osobiście bardziej polegam na słowniku, który sam sobie wpiszę niż na jakimś niby łatwiejszym rozwiązaniu, za które tak naprawdę nikt nie bierze odpowiedzialności.
Szkoda, że język/biblioteka standardowa nie udostępnia pewnego rozwiązania tego zdawałoby się prostego problemu. No, ale czego się spodziewać, w końcu biblioteka standardowa nie zapewnia nawet możliwości alfabetycznego sortowania słów.
No ja też jestem ciekaw, dlatego spytałem. Chodzi mi o jakiś ogólny opis algorytmu, bo odczytywanie zdekompilowanych metod nie jest zbyt wygodne.
Nie. Moim celem było pokazanie elastycznego i łatwego do rozbudowy rozwiązania nie posiadającego skomplikowanych instrukcji warunkowych.
Nie ma to jak tendencyjny dobór danych testowych.
Ale dobra, wyłącz ignorowanie cyfr i zobacz kto teraz będzie szybszy. Masz coś nie tak z zapisem, czy jak?
Obiektowa struktura mojej aplikacji powoduje wielokrotne wywoływanie metod, a każde z nich “kosztuje”. Za to jest łatwiejsza w rozbudowie. Coś za coś.
Nie wydaje mi się, u mnie wywalił drugą linijkę.
Sprawdzam Twoją czujność.
Ale skoro już przy błędach jesteśmy, to moim zdaniem, przy włączeniu wszystkich 5 opcji, wszystkie linijki takiego pliku:
ala ma kota
ALA MA KOTA
123ALA MA KOTA123
ąłą mą kótą
ĄŁĄ MĄ KÓTĄ
są identyczne. Tymczasem Twój program twierdzi, że ostatnia jest inna.
W celu realnego testowania wydajności przygotowałem taki plik: http://somekind.pl/_upload/dp/pt1c.7z
Plik ten ma tysiąc razy więcej wersów niż pierwsza księga Pana Tadeusza, która jest źródłem danych dla niego. Wersy oryginalne zostały umieszczone tam w kolejności losowej, na dodatek losowo dodano do nich cyfry, spacje i umieszczono więcej znaków diakrytycznych.
Na początku mój program był wolniejszy od Twojego, ale po usunięciu wszystkich Regexów i zmiany algorytmu usuwającego diakrytyki na wersję z char[] zamiast StringBuilder, przyspieszył prawie trzykrotnie i jest szybszy o ok. 20%. (A poza tym tylko moja wersja generuje prawidłowy wynik.)
Nowa wersja: http://somekind.pl/_upload/dp/Duplicate … tion1.1.7z
Fakt.
Opiszę Ci to w przyszłym tygodniu, bo szczerze to nie pamiętam jak ja dokładnie to rozwiązałem. To było kilka miesięcy temu
Nie rozumiem tego, czy mam coś nie tak z zapisem? Co masz na myśli? A testów nie dobierałem tendencyjnie. To był drugi z dwóch. Pierwszy był na pliku 250 MB. Chciałem zobaczyć jak duże będziesz miał zużycie RAM. Sprawdź sobie (ładowanie całego pliku do pamięci jest złym rozwiązaniem - też tak na początku miałem, ale to później zmieniłem). Jeśli uważasz, że dobierałem tendencyjnie to przeprowadź Swoje. Takie zarzuty są co najmniej nie fair. Z poprzedniego postu można było wywnioskować, że Twoje rozwiązanie zapewni o wiele lepsze czasy. Wygenerowałem więc sobie plik i okazało się, że tak po prostu nie jest. Po co miałem przeprowadzać więcej testów?
Kurczę no mówię Ci, że u mnie to nie działa - Win7 64 bit PL. Sprawdzę w przyszłym tygodniu nową wersję kodu, którą dałeś.
Masz rację z jakiegoś tajemniczego powodu w pliku diactrics.xml dodałem małe ł, a nie dodałem wielkiego Ł. Wstaw sobie ręcznie i powinno być OK.
Wyniki powinny być poprawne jak zrobisz to co podałem wyżej. Ja potestuję to w przyszłym tygodniu, bo muszę wprowadzić zmianę w kodzie. Zauważ, że na każdym string-u muszę przeprowadzić kilka dodatkowych operacji, wynikających z tego, że chcę uzyskać uniwersalność w tym wykrywaniu znaków diaktrycznych (yyy diakrytycznych). To może powodować (i pewno powoduje) pewien narzut - możliwe, że nawet całkiem spory. W każdym bądź razie na pewno dam znać o wynikach.
Pracując na pliku, w którym są same cyfry z włączoną opcją “ignoruj cyfry” program tworzy plik wyjściowy z jednym wierszem. Po wyłączeniu tej opcji, w pliku wyjściowym znajdują się wszystkie wiersze z pliku wejściowego. Działanie Twojego programu w pierwszym przypadku jest szybkie, w drugim powolne. Wysnułem hipotezę, że Twój program spowolniony jest przez zapis… Ale jak teraz patrzę w zdekompilowany kod, to dochodzę do wniosku, że to jednak raczej przez to, że po wyłączeniu opcji sprawdzane są wszystkie warunki w Twoim ogromnym ifie, a nie tylko dwa pierwsze, i to może tak spowalniać kod.
Ależ ja nie muszę tego sprawdzać, wiem co robię i doskonale zdaję sobie sprawę z tego, że skoro wczytuję cały plik do pamięci, to on się w niej znajduje. Mój program nadaje się do plików określonej wielkości, w przypadku większych trzeba je oczywiście przetwarzać sekwencyjnie.
Nie miałem nawet zamiaru napisać programu użytkowego, chciałem tylko przedstawić koncepcję projektu aplikacji.
Jeśli wczytasz się w poprzedniego posta, to zauważysz, że to zrobiłem, nawet plik testowy upubliczniłem.
Masz rację, mój program, pod względem wydajnościowym nie jest aż tak rewelacyjny, jak myślałem, że będzie. Ale wciąż jest czytelniejszy i łatwiejszy w modyfikacji.
Sprawdzałem również na starej i to akurat działało.
Faktycznie, w tym był problem. Coś nam obu ta litera nie leży.
Na początek drobne wyjaśnienie … ja u siebie w programie, gdy napotykam linię w pliku, która zawiera wyłącznie spacje i tabulatory (czyli ogólnie same białe znaki), to traktuję ją jako pustą linię (zwiększany jest licznik Empty rows count) - choć teraz pojawiła się opcja w programie, która pozwala to sprecyzować. Z tego co widziałem, to Ty traktujesz taką linię jak każdą inną, a więc stosujesz do niej wszystkie warunki. Innych różnic w sposobie przetwarzania pomiędzy naszymi programami nie zauważyłem. Błędów w sposobie przetwarzania też nie zauważyłem. Teraz do rzeczy
dodałem lokalizację językową - polski i angielski, a tym samym uporządkowałem etykiety,
zastapiłem słownikami znaków diakrytycznych ten ww. uniwersalny sposób wykrywania znaków diakrytycznych. Słowniki można edytować. Przy każdym słowniku jest informacja o tym, czy ma być aktywny, czy nie. Słownik aktywny to taki, który będzie użyty w trakcie przetwarzania pliku. Przykładowo baza słowników może składać się z 10 słowników, ale w danym momencie używany może być np. tylko jeden wybrany albo np. trzy jakieś tam wybrane. Na starcie w bazie słowników znajduje się tylko jeden słownik, a mianowicie słownik znaków diakrytycznych w języku polskim. Jest on ustawiony jako aktywny (można oczywiście go wyłączyć).
log jest bardziej szczegółowy, m.in. widnieje w nim teraz informacja o tym jakie parametry oraz jakie słowniki znaków diakrytycznych zostały zastosowane w trakcie przetwarzania.
poprawiłem wydajność zarówno w sposobie przetwarzania opartym o większe zużycie RAM, jak i w sposobie opartym o większe zużycie HDD (przy okazji checkbox w GUI zamieniłem na radiobutton)
dodałem możliwość precyzowania czym jest pusty wiersz, a mianowicie, czy jest to wiersz, który zawiera tylko i wyłącznie znak nowej linii, czy też jest to wiersz, który zawiera dowolny biały znak.
Jest jeszcze kilka rzeczy, które muszą zostać zrobione, ale to będę poprawiać/dodawać, gdy ktoś zgłosi taką potrzebę albo znajdę czas.
Jest to sposób analogiczny do tego, który oparty jest o większe zużycie RAM. Zasadnicza różnica jest taka, że tutaj kolekcję HashSet zastępują pliki. Jak to się dzieje? Biorę string, który ma zostać przetworzony i przetwarzam go. Dla takiego przetworzonego string-a wyliczam jego hash i sprawdzam, czy istnieje plik o nazwie, która jest równa temu hash-owi, czyli np. hash przetworzonego string-a to 12334444, więc ja sprawdzam, czy istnieje plik 12334444.txt. I teraz:
jeśli plik nie istnieje to tworzę go i zapisuję w nim ten przetworzony string. Operacja ta oznacza, że ten przetworzony string to wiersz unikalny - taki który do tej pory się nie pojawił.
jeśli plik już istnieje, to może to oznaczać dwie rzeczy:
a) w pliku tym znajduje się identyczny ciąg znaków jak ten przetworzony string
b) w pliku tym znajduje się inny ciąg znaków, dla którego wyliczany jest identyczny hash jak dla tego przetworzonego string-a.
Co więc robię? Odczytuję zawartość tego pliku linijka po linijce i sprawdzam, czy taki przetworzony string już się tam znajduje. Jeśli się nie znajduje, to dodaję go i tym samym oznacza to, że znaleziony został unikalny wiersz - taki który do tej pory się nie pojawił. Jeśli się znajduje, to oznacza, że ten przetworzony string to duplikat jakiegoś innego string-a, a więc nic z nim nie robię.
Ogólnie, żeby wprowadzić te zmiany musiałem przebudować kod aplikacji. Teraz wyróżnione są w niej niejako trzy warstwy klas. Pierwsza warstwa klas odpowiedzialna jest za wczytywanie pliku. Druga warstwa klas odpowiedzialna jest za przetwarzanie pliku (sposób oparty o RAM lub HDD - może być ich oczywiście więcej). Trzecia warstwa klas odpowiedzialana jest za przetwarzanie wiersza (te wszystkie warunki Ignoruj). Pierwsza warstwa klas korzysta z klas drugiej warstwy, a druga warstwa klas korzysta z klas trzeciej warstwy.
Plik zawierający liczby od 1 do 1mln:
a) żaden parametr nie jest wybrany
Moje czasy (3 próby)
00:00:03.4632060
00:00:03.5568063
00:00:03.4944061
Najsłabszy: 00:00:03.5568063
Twoje czasy (3 próby)
00:00:00.5162906
00:00:00.5195054
00:00:00.5318744
Najsłabszy: 00:00:00.5318744
Zwycięzca: 00:00:00.5318744
b) wszystkie parametry są wybrane
Moje czasy (3 próby)
00:00:00.2184003
00:00:00.2340004
00:00:00.2184004
Najsłabszy: 00:00:00.2340004
Twoje czasy (3 próby)
00:00:00.9378668
00:00:00.8122258
00:00:00.9037621
Najsłabszy: 00:00:00.9378668
Zwycięzca: 00:00:00.2340004
c) wszystkie parametry oprócz Ignoruj cyfry są wybrane
Moje czasy (3 próby)
00:00:04.3368076
00:00:04.3524077
00:00:04.3680077
Najsłabszy: 00:00:04.3680077
Twoje czasy (3 próby)
00:00:01.7498786
00:00:01.6753115
00:00:01.7120607
Najsłabszy: 00:00:01.7498786
Zwycięzca: 00:00:01.7498786
Plik podany przez Ciebie
a) żaden parametr nie jest wybrany
Moje czasy (3 próby)
00:00:01.8720033
00:00:01.8876034
00:00:01.9032033
Najsłabszy: 00:00:01.9032033
Twoje czasy (3 próby)
00:00:01.2578968
00:00:01.3082743
00:00:01.2586645
Najsłabszy: 00:00:01.3082743
Zwycięzca: 00:00:01.3082743
b) wszystkie parametry są wybrane
Moje czasy (3 próby)
00:00:03.2448057
00:00:03.2292057
00:00:03.2604058
Najsłabszy: 00:00:03.2604058
Twoje czasy (3 próby)
00:00:05.5060891
00:00:05.5327697
00:00:05.6051977
Najsłabszy: 00:00:05.6051977
Zwycięzca: 00:00:03.2604058
c) wszystkie parametry oprócz Ignoruj cyfry są wybrane
Moje czasy (3 próby)
00:00:04.3836077
00:00:04.4304077
00:00:04.3680077
Najsłabszy: 00:00:04.4304077
Twoje czasy (3 próby)
00:00:05.1033447
00:00:05.2415330
00:00:05.2663061
Najsłabszy: 00:00:05.2663061
Zwycięzca: 00:00:04.4304077
Wnioski: Prawdę mówiąc ciężko cokolwiek powiedzieć Za mało testów. Sęk w tym, że więcej mi się nie chce na razie przeprowadzać - dobija mnie to rekompilowanie i przepisywanie wyników. W każdym bądź razie … szczególnie zastanawia mnie dlaczego uzyskujesz lepsze czasy, gdy nie są wybrane żadne parametry (ja wtedy nie wykonuję żadnych operacji na wierszu). W sensie co to powoduje - na pewno będę tutaj szukał przyczyny. Choć widać, że w miarę, gdy mamy co raz większy plik ta różnice maleje. Gdy wybrane są wszystkie parametry, to ja uzyskuję lepsze czasy. Choć tutaj też widać, że ta różnica maleje. Gdy jeden parametr nie jest wybrany, to już w ogóle są jaja, bo raz uzyskałeś lepszy czas Ty, a raz ja. Jedno jest pewne, czasy w poszczególnych próbach uzyskujemy oboje w miarę stabilne. Pamiętam, że jak przeprowadzałem ostatnio testy na Twoim programie, to te wyniki bardzo Ci skakały. Zgaduję, że przyczyną tego, był silnik wyrażeń regularnych. Teraz, gdy zamieniłeś wyrażenia regularne na pętlę po char-ach, różnice w poszczególnych próbach są minimalne. Na zakończenie zachęcam Cię do przeprowadzenia innych testów i podzielenia się wynikami. Może da się wyciągnąć jakieś konkretniejsze wnioski.
– Dodane 31.12.2011 (So) 18:07 –
Znalazłem winowajcę spowolnienia operacji przetwarzania w sytuacji, gdy nie wybrany został żaden parametr. Jest nim (przede wszystkim) wbudowana metoda Flush(). Na początku przyszłego tygodnia wrzucę zaktualizowaną wersję i wykonam nowe testy.
– Dodane 02.01.2012 (Pn) 19:40 –
Nowa wersja programu: Duplicate Finder v1.3.1.
Zaktualizowane testy:
Plik zawierający liczby od 1 do 1 mln:
a) żaden parametr nie jest wybrany
Moje czasy (3 próby)
00:00:00.7020013
00:00:00.6864012
00:00:00.7020013
Najsłabszy: 00:00:00.7020013
Twoje czasy (3 próby)
00:00:00.5206313
00:00:00.5363469
00:00:00.5200859
Najsłabszy: 00:00:00.5363469
Zwycięzca: 00:00:00.5363469
b) wszystkie parametry są wybrane
Moje czasy (3 próby)
00:00:00.2340004
00:00:00.2340004
00:00:00.2184004
Najsłabszy: 00:00:00.2340004
Twoje czasy (3 próby)
00:00:00.8458083
00:00:00.8284389
00:00:00.8127624
Najsłabszy: 00:00:00.8458083
Zwycięzca: 00:00:00.2340004
c) wszystkie parametry oprócz Ignoruj cyfry są wybrane
Moje czasy (3 próby)
00:00:01.1700021
00:00:01.1856021
00:00:01.1388020
Najsłabszy: 00:00:01.1856021
Twoje czasy (3 próby)
00:00:01.6609576
00:00:01.6545139
00:00:01.6511012
Najsłabszy: 00:00:01.6609576
Zwycięzca: 00:00:01.1856021
d) wszystkie parametry oprócz Ignoruj cyfry oraz Ignoruj znaki diakrytyczne są wybrane
Moje czasy (3 próby)
00:00:01.0764019
00:00:01.1076019
00:00:01.0452019
Najsłabszy: 00:00:01.1076019
Twoje czasy (3 próby)
00:00:01.4821992
00:00:01.4877286
00:00:01.4807600
Najsłabszy: 00:00:01.4877286
Zwycięzca: 00:00:01.1076019
e) wybrane są tylko parametry: Ignoruj znaki interpunkcyjne, Ignoruj białe znaki
Moje czasy (3 próby)
00:00:00.8892016
00:00:00.8736016
00:00:00.8736015
Najsłabszy: 00:00:00.8892016
Twoje czasy (3 próby)
00:00:01.3148484
00:00:01.2921848
00:00:01.2967938
Najsłabszy: 00:00:01.3148484
Zwycięzca: 00:00:00.8892016
Plik podany przez Ciebie
:
a) żaden parametr nie jest wybrany
Moje czasy (3 próby)
00:00:01.1232020
00:00:01.1388020
00:00:01.1232020
Najsłabszy: 00:00:01.1388020
Twoje czasy (3 próby)
00:00:01.2657101
00:00:01.2535684
00:00:01.2537009
Najsłabszy: 00:00:01.2657101
Zwycięzca: 00:00:01.1388020
b) wszystkie parametry są wybrane
Moje czasy (3 próby)
00:00:03.2136057
00:00:03.1980056
00:00:03.2136056
Najsłabszy: 00:00:03.2136057
Twoje czasy (3 próby)
00:00:05.4762696
00:00:05.5006639
00:00:05.4927574
Najsłabszy: 00:00:05.5006639
Zwycięzca: 00:00:03.2136057
c) wszystkie parametry oprócz Ignoruj cyfry są wybrane
Moje czasy (3 próby)
00:00:03.6036063
00:00:03.5412063
00:00:03.5412062
Najsłabszy: 00:00:03.6036063
Twoje czasy (3 próby)
00:00:05.1135168
00:00:05.1284255
00:00:05.1193364
Najsłabszy: 00:00:05.1284255
Zwycięzca: 00:00:03.6036063
d) wszystkie parametry oprócz Ignoruj cyfry oraz Ignoruj znaki diakrytyczne są wybrane
Moje czasy (3 próby)
00:00:02.5116044
00:00:02.4492043
00:00:02.4648043
Najsłabszy: 00:00:02.5116044
Twoje czasy (3 próby)
00:00:03.7493809
00:00:03.6719107
00:00:03.7087046
Najsłabszy: 00:00:03.7493809
Zwycięzca: 00:00:02.5116044
e) wybrane są tylko parametry: Ignoruj znaki interpunkcyjne, Ignoruj białe znaki
Moje czasy (3 próby)
00:00:02.1684038
00:00:02.1996039
00:00:02.1840039
Najsłabszy: 00:00:02.1996039
Twoje czasy (3 próby)
00:00:03.3119890
00:00:03.3204452
00:00:03.2973431
Najsłabszy: 00:00:03.3204452
Zwycięzca: 00:00:02.1996039
Wnioski: Jak widać po wprowadzeniu drobnej poprawki w programie, wyniki testów zostały zdominowane przez kolor zielony (choć istotne przy tym jest, że w kilku testach różnica czasu nie przekroczyła 0.5 sekundy). Udało Ci się uzyskać lepszy czas tylko w jednym z testów. Dlaczego tak się stało? Na razie wniosków nie będę wyciągał, poczekam na więcej testów (wciąż jest ich za mało, mimo tego, że dodałem dwa dodatkowe).
Też ostatnio to zauważyłem i zmieniłem, również zacząłem pomijać puste wiersze, więc różnic żadnych już nie ma.
No fajnie, ale wypadałoby przy pierwszym uruchomieniu wykryć język używany w systemie i ten zastosować, a nie na siłę angielski.
Słusznie, tak jest dużo czytelniej. Mogę jeszcze zasugerować zmianę tych checkboxów od opcji “znakowych” na CheckedListBox, żeby można było szybko zaznaczać wszystkie opcje z użyciem Shifta.
W jaki sposób poprawiłeś wydajność?
Strzelam, że gdyby zamiast wielu plików był jeden, to wydajność znacznie by wzrosła. Obecna jest po prostu zasmucająca.
Z taką ilością warstw można z tego zrobić pracę inżynierską.
Wybacz czepialstwo, ale jeśli jesteś mężczyzną, to używaj, proszę, formy “obaj”.
Tja… I w ten sposób napisałeś malware.
Wykonujesz masę operacji plikowych, które nie są flushowane, więc zapychają bufor dyskowy. Twój program udaje, że zużywa jedynie 17 MB pamięci, ale u mnie dla pliku pt1c.txt zeżarło 2GB, a po zakończeniu procesu oddało 1 GB z tego.
Najnowsza wersja mojego programu: http://somekind.pl/_upload/dp/DuplicatedRowsKillerTurboEdition.1.2.7z
Poza pomijaniem pustych wierszy dodałem jedną optymalizację - z uwagi na to, że bardzo często używane są wszystkie opcje, dodałem klasę FullTransformer.
Nie przeprowadzałem testów na pliku z liczbami, bo nie mają one moim zdaniem sensu. Użyłem pliku pt1c.txt oraz pt1c_big.txt, który powstał na podobnych zasadach, z tymże jest 20 razy większy (ma nieco ponad 1GB - spakowany do pobrania tutaj: http://somekind.pl/_upload/dp/pt1c_big.7z, ok. 100 MB).
Wyniki w tej tabelce: https://docs.google.com/spreadsheet/ccc?key=0AmaLfUqJeO8FdGpERE43R0VRVnRUalM0b0NyZE93ZGc#gid=0
Wnioski:
Ładowanie całego pliku na raz, nie tylko zużywa więcej pamięci, ale jest wolniejsze niż przetwarzanie sekwencyjne.
Twoja wersja 1.2 jest wolniejsza od mojej pierwotnej wersji w każdym przypadku.
Twoja wersja 1.2 w przypadku wybrania opcji “większego zużycia HDD” w przypadku nie wybrania żadnej opcji faktycznie oszczędza pamięć RAM. Przy zaznaczeniu wszystkich opcji jest wręcz odwrotnie.
Użycie opcji “większego zużycia HDD” generalnie pozbawia aplikację czegoś takiego jak wydajność.
Twoja wersja 1.3.1 - w przypadku nie wybrania żadnej opcji, i tak jestem minimalnie szybszy.
Twoja wersja 1.3.1 - dla wszystkich opcji Twój program jest szybszy o jakieś 30%.
Ostateczny zwycięzca - DuplikatedRowsKillerTurboEdition, bo jest Open Source.
Mam pomysł na dwie optymalizacje swojego kodu, ale nie wierzę w nie za bardzo. W wolnej chwili zaimplementuję.
Niemniej jednak, wciąż jestem przekonany, że moje algorytmy modyfikujące wiersze są czytelniejsze od Twojego superifa, oraz że mi jest łatwiej dopisać nowe warunki. I być może wtedy mój program okazałby się wydajniejszy… Może na przykład opcje: ignorowania nawiasów czterech typów oraz ignorowania znaków działań, a ponadto słów nie mogących być nazwami zmiennych w C?
Ja ich nie pomijam. W moim programie masz opcję, która pozwala wybrać, czy chcesz pomijać wiersze zawierające wyłącznie puste znaki, czy nie. Wszystkie moje powyższe testy były przeprowadzone bez pomijania takich wierszy.
Zanotowałem sobie.
W sposobie opartym o większe zużycie RAM (co w zasadzie miało też wpływ na sposób oparty o większe wykorzystanie HDD) przede wszystkim użyłem słowników zamiast uniwersalnego sposobu usuwania znaków diakrytycznych. Tak jak podejrzewałem, w jednym z moich poprzednich postów, to miało znaczny wpływ na ogólną wydajność (funkcjonalność za wydajność). Poza tym wprowadziłem trochę drobnych zmian w kodzie, który dały niewielki wzrost wydajności - do 100 ms.
Proponuję, żebyś to sprawdził i podzielił się wynikami
Przy takiej strukturze kodu, łatwiej wprowadza się kolejne zmiany.
Ciekawe, zerknę na to. Mówisz o metodzie opartej o HDD? Ja usunąłem wprawdzie wywołanie Flush(), ale plik jest zamykany (przy użyciu metody Close()) po zakończeniu operacji zapisu.
Jeszcze raz powtarzam - ja nie pomijam żadnych wierszy (chyba, że sam sobie wybierzesz, żeby wiersze zawierające tylko i wyłącznie białe znaki, były pomijane).
Jak wygląda kod tej klasy? Ta klasa korzysta z klas, które miałeś napisane wcześniej? Ja u siebie zastanawiałem się, czy nie wprowadzić klas, które będą wywoływane w sytuacji, gdy został wybrany tylko jeden parametr. Sęk w tym, że wtedy w dwóch miejscach miałbym ten sam warunek, więc z tego zrezygnowałem.
Co to znaczy, że test był niemożliwy do przeprowadzenia? I co to jest SK oraz SK FT?
Tak jak pisałem powyżej. Zerknę na to. Ja usunąłem wywołanie Flush po każdym WriteLine, ale mimo tego plik jest zawsze zamykany (Close) po zakończeniu operacji zapisu.
To jest dość oczywiste. Operacje na pamięci są szybsze niż operacje na HDD. Wciąż jednak uważam, że dałoby się podnieść wydajność tego sposobu (na pewno poprzez zastosowanie szybszego dysku twardego ) poprzez zastosowanie innego algorytmu. Sęk w tym, że mi się takowego nie udało wykombinować, dlatego jak już wyżej pisałem, jestem otwarty na dobre propozycje.
Ja sobie lubię żartować, że .NET wspiera open source.
Być może to i być może tamto To nie o to chodzi. Parametrów mamy teraz dużo, bo aż 5. Jest więc na czym porównywać wyniki. Oprócz tego, że można wybierać różną ilość parametrów, można je jeszcze łączyć w różnych kombinacjach. Ja dlatego napisałem wyżej, że nie wyciągam na razie więcej wniosków, bo liczyłem na to, że Ty przeprowadzisz więcej testów. Aha a ten superif (a w zasadzie metoda odpowiedzialna za przetwarzanie wiersza) wygląda teraz tak, gdy został wybrany przynajmniej jeden parametr:
public override string Modify(string originalRow)
{
char[] modifiedRow = new char[originalRow.Length];
int j = 0;
for (int i = 0; i < originalRow.Length; i++)
{
char c = originalRow[i];
if (!(
(Parameters.IgnoreWhitespace && char.IsWhiteSpace(c)) ||
(Parameters.IgnoreDigits && char.IsDigit(c)) ||
(Parameters.IgnorePunctuationMarks && char.IsPunctuation(c))
))
{
if (Parameters.IgnoreDiacriticalMarks && DiacriticsContainer.Instance.ContainsKey(c))
c = DiacriticsContainer.Instance[c];
modifiedRow[j] = c;
j++;
}
}
originalRow = new string(modifiedRow, 0, j);
if (Parameters.IgnoreCase && !string.IsNullOrEmpty(originalRow))
originalRow = originalRow.ToLower(CultureInfo.CurrentCulture);
return originalRow;
}
Natomiast tak, gdy nie został wybrany żaden parametr:
public override string Modify(string originalRow)
{
return originalRow;
}
Metody te znajdują się w klasach Basic- i ComplexRowModifier.
W aktualnej wersji programu, pustych wierszy nie przetwarzam żadnymi algorytmami, za to zliczam ich wystąpienia i nie umieszczam ich w wynikowym pliku. To nazywam “pomijaniem”. Z tego co zauważyłem, to wyniki działania naszych programów są teraz identyczne.
Mam zamiar, chociaż nie wiem, czy znajdę na to prędko czas.
Oczywiście, jak najbardziej popieram warstwowość. Chociaż w tak prostym narzędziu jest ona trochę przerostem formy nad treścią. A słowa o inżynierce nie są tak do końca żartem.
A kiedy te operacje się kończą? Tworzysz kilkaset tysięcy plików, trzymasz je ciągle otwarte czy robisz to dopiero na żądanie? Wiesz, coś musi być przyczyną, nic się samo nie dzieje.
W ogóle, to pliki tymczasowe chyba ładniej trzymać w %temp%, a nie w katalogu obok przetwarzanego pliku.
No niestety, nie korzysta. Jest hackiem wydajnościowym, przez co łamie nieco elastyczność, a przede wszystkim zasadę DRY:
class FullTransformer : IRowTransformingStrategy
{
private static readonly Dictionary diacristics = new Dictionary
{
{ 'ą', 'a' }, { 'ę', 'e' }, { 'ć', 'c' }, { 'ł', 'l' }, { 'ń', 'n' }, { 'ó', 'o' }, { 'ś', 's' }, { 'ź', 'z' }, { 'ż', 'z' },
{ 'Ą', 'A' }, { 'Ę', 'E' }, { 'Ć', 'C' }, { 'Ł', 'L' }, { 'Ń', 'N' }, { 'Ó', 'O' }, { 'Ś', 'S' }, { 'Ź', 'Z' }, { 'Ż', 'Z' },
};
public string Transform(string row)
{
StringBuilder sb = new StringBuilder();
string temp = row.ToLower(CultureInfo.InvariantCulture);
foreach (char c in temp)
{
if (!char.IsDigit(c) && !char.IsPunctuation(c) && !char.IsWhiteSpace(c))
{
if (diacristics.ContainsKey(c))
{
sb.Append(diacristics[c]);
}
else
{
sb.Append(c);
}
}
}
return sb.ToString();
}
}
SK jak somekind. SK FT to wersja z operacjami przyspieszonymi przy użyciu powyższej klasy (w przypadku wybrania wszystkich parametrów).
Test niemożliwy do przeprowadzenia oznacza, że foobar ma problemy z ciągłością odtwarza, system staje się nieresponsywny, a 8GB RAM przestaje wystarczać. Wtedy zabiłem proces. (I zrestartowałem komputer, bo pamięć nadal była zajęta.) Może to przez to, że mam tylko i5, a nie i7?
No i przeprowadziłem. Można testować też różne kombinacje parametrów, ale całe to kilkanie sporo czasu zajmuje, przydałby się jakiś automat.
Ale zaraz, zaraz… Czy to znaczy, że boisz się wprowadzać zmiany do swojego programu?
No cóż, skoro tak uważasz
No właśnie nie. Zaraz po otwarciu pliku (jednego z tych kilkuset tysięcy, a nawet milionów) i wprowadzeniu w nim potrzebnych zmian jest on zamykany. Zresztą widać to bardzo dobrze w kodzie. Jest to klasa HybridModifiedRowsProcessor. Wygląda ona dokładnie tak (tak przy okazji - to jest właśnie klasa z drugiej warstwy klas; widać, że korzysta ona z klas trzeciej warstwy):
using System;
using System.Collections.Generic;
using System.IO;
namespace DuplicateFinderCore.FileProcessingLogic
{
internal class HybridModifiedRowsProcessor : ModifiedRowsProcessor
{
private string copyDirectoryPath;
private string copyFileExtension;
public HybridModifiedRowsProcessor(DuplicateFinderParameters parameters)
: base(parameters)
{
copyDirectoryPath = GetDefaultCopyDirectoryPath();
copyFileExtension = GetDefaultCopyFileExtension();
if (!Directory.Exists(copyDirectoryPath))
Directory.CreateDirectory(copyDirectoryPath);
}
public override bool Add(string originalRow)
{
string modifiedRow = Modifier.Modify(originalRow);
int modifiedRowHash = modifiedRow.GetHashCode();
string modifiedRowHashString = modifiedRowHash.ToString();
string filePath = GetDefaultCopyFilePath(modifiedRowHashString);
FileHandle fileHandle = null;
bool isDistinctRow = true;
try
{
if (File.Exists(filePath))
{
fileHandle = new FileHandle(filePath, FileMode.Open, FileAccess.ReadWrite, FileShare.None);
fileHandle.CreateStreamReader(Parameters.FileEncoding);
string modifiedRowCopy = string.Empty;
while ((modifiedRowCopy = fileHandle.ReadLine()) != null)
{
if (modifiedRowCopy.Length == modifiedRow.Length
&& modifiedRowCopy.Equals(modifiedRow, StringComparison.Ordinal))
{
isDistinctRow = false;
break;
}
}
if (isDistinctRow)
{
fileHandle.CreateStreamWriter(Parameters.FileEncoding);
fileHandle.WriteLine(modifiedRow);
}
}
else
{
fileHandle = new FileHandle(filePath, FileMode.Create, FileAccess.Write, FileShare.None);
fileHandle.CreateStreamWriter(Parameters.FileEncoding);
fileHandle.WriteLine(modifiedRow);
}
}
catch (Exception ex)
{
throw ex;
}
finally
{
if (fileHandle != null)
fileHandle.Close();
}
return isDistinctRow;
}
public override void Clear()
{
if (Directory.Exists(copyDirectoryPath))
Directory.Delete(copyDirectoryPath, true);
}
private string GetDefaultCopyDirectoryPath()
{
return Path.Combine(Path.GetDirectoryName(Parameters.OutputFilePath), "Copy");
}
private string GetDefaultCopyFileExtension()
{
return Path.GetExtension(Parameters.OutputFilePath);
}
private string GetDefaultCopyFilePath(string name)
{
return Path.Combine(copyDirectoryPath, string.Format("{0}{1}", name, copyFileExtension));
}
}
}
Ale mówię, sprawdzę to, o czym piszesz. Być może faktycznie, gdzieś coś przeoczyłem, co powoduje skok zużycia RAM.
Tak, sęk w tym, że tych plików nie można traktować jak zwykłe pliki tymczasowe. Przede wszystkim plik o nazwie równej jakiemuś tam hash-owi może już znajdować się w katalogu Temp.
A skoro już pojawił się temat folderu, to nie wiem jeszcze jaki wpływ na wydajność ma w tym przypadku indeksowanie zawartości folderu. To jest coś, co też przy okazji muszę sprawdzić i przemyśleć. I jeszcze jedna rzecz … możesz mi powiedzieć, czy Ty te pliki, które testujesz, masz na partycji systemowej, czy jakiejś innej?
Tak też sądziłem. IMO powoduje to, że test w sytuacji, gdy wybrane są wszystkie parametry, jest niewiarygodny. Zauważ, że cały czas podkreślasz, że Twój kod jest bardziej czytelny i łatwiejszy w modyfikacji. W tym wypadku zrezygnowałeś z tej łatwości modyfikacji, żeby podbić sobie wyniki. Co innego gdyby ta klasa korzystała z tych klas, które napisałeś wcześniej - ja właśnie coś takiego chcę mieć prędzej, czy później u siebie. Wtedy nie mógłbym się przyczepić.
Czyli Ty w trakcie przeprowadzania tych testów miałeś włączone różne aplikacje? A chociaż były one takie same, czy się zmieniały?
Jednak dobrze, że takie zachowanie zaobserwowałeś (za co dziękuję). Jak już pisałem wyżej, sprawdzę to na pewno.
No ale co z wynikami? Bo przecież tu chodzi o wyniki. Jak ostatnio testowałem Twoją aplikację, to nie zauważyłem żadnego automatu. To najwyraźniej mój błąd. Jak mam z niego skorzystać? Ja ręcznie modyfikowałem kod (zaznaczałem, które parametry mają być aktywne, a które nie), rekompilowałem go, a potem przepisywałem wyniki i dlatego nie chciało mi się przeprowadzać więcej testów.
A zmian wierz mi, wprowadzać się nie boję. Zresztą chyba to do tej pory to zauważyłeś Chociaż jeśli miałbym zrobić automat, to pewno byłaby to osobna aplikacja konsolowa, która korzysta z biblioteki DuplicateFinderCore.dll (która jest dołączona razem z aplikacją).
Więc może chodzi o to, że w przypadku zapisu do pliku wynikowego, również nie flushujesz po każdej linii, lecz dopiero na koniec.
Zawsze można zrobić podkatalog.
Zauważyłem znaczne skoki zużycia procesora przez explorer.exe podczas pracy Twojego programu, więc pewno też ma.
Na innej.
Myślę, że mój kod mimo tego hacka pozostaje czytelniejszy i łatwiejszy w modyfikacji. Po prostu przy dodaniu nowego sposobu transformacji trzeba wprowadzić ją w dwóch miejscach, nie w jednym.
No przecież nie będę likwidował stanowiska pracy na kilkanaście godzin. Aplikacje to foobar, Firefox i Word, nie otwierałem ani nie wykonywałem żadnych większych operacji w trakcie testów.
Najpierw trzeba go napisać, żeby móc korzystać. Chodzi mi o to, że zarówno rekompilacja mojego kodu jak i konieczność restartowania Twojego programu i wyklikiwania w nim opcji są żmudne i czasochłonne. Do porównawczych testów wydajności przydałby się jakiś program/skrypt testujący, który automatycznie wywoływałby te programy, ustawiał im opcje i odbierał wyniki.
No tak, GUI jest coraz lepsze, są warstwy, wartościowe logi… Ale mi chodzi o zmiany w najważniejszej części aplikacji.
Słuszna uwaga, to może być to. Jutro na to popatrzę.
Haha, fakt Ale jednak zostawię póki co, tak jak jest teraz. Użytkownik w końcu u mnie w aplikacji wybiera sobie, gdzie ma zostać zapisany plik wynikowy i tam też tworzę log oraz folder Copy na pliki z hash-ami.
Choć w sumie jak tak teraz myślę, to najlepiej by było wykonać sprawdzenie - jeszcze przed rozpoczęciem operacji przetwarzania, czy na docelowej partycji jest wystarczająca ilość miejsca. Aktualnie takiego sprawdzenia nie wykonuję (zakładałem, że użytkownik dokona poprawnego wyboru folderu). To zostanie dodane w nowej wersji programu.
No dobra, ale jaki ma wpływ - pozytywny czy negatywny? Czy lepiej, żeby zawartość tego folderu była indeksowana, czy też nie? To właśnie chcę sprawdzić.
Nie dasz sobie nic powiedzieć. Nie wierzę, że uważasz, że to jest prostsze w modyfikacji. Na prawdę sądzisz, że lepiej jest tą samą zmianę wprowadzać w dwóch miejscach? Kurczę to Ty zacząłeś dyskusję, że mój kod jest trudny w modyfikacji, a teraz podajesz rozwiązanie, które przecież jest złe. Rozwiązanie, które opisałeś niedawno (w sensie z tym wywoływaniem w pętli warunków) uważam za ładne i teraz testy mają wykazać, czy jest też wydajniejsze. A jeśli jest, to w jakiej sytuacji. Jeśli zaczniesz w nim wprowadzać takie udziwnienia to dalsze testy po prostu stracą sens.
To są Twoje słowa sprzed kilku miesięcy:
Właśnie chodzi o to, że w tym konkretnym przypadku nie wierzę. Te testy mają wykazać, czy lepsza jest pojedyncza pętla na danym wierszu z kilkoma if-ami, czy też kilka pętli na pojedynczym wierszu bez żadnego if-a (a raczej z prostszym warunkiem w if-ie) - przynajmniej ja zwracam na to duża uwagę. Cały czas przy tym powtarzam, że wstrzymuję się z wnioskami, bo jest za mało testów.
Jeśli Twój sposób okaże się wydajniejszy w pewnych przypadkach (bo jak widać w przypadku, gdy wybrane są wszystkie parametry tak nie jest), to z chęcią pomyślę nad tym jak te zmiany zaadoptować u siebie. Ja na prawdę jestem otwarty na propozycje, inaczej nie toczyłbym z Tobą dyskusji
Rozumiem, nie będę ciągnął tego wątku
Aha, myślałem, że masz już napisany taki automat, bo z poprzedniego Twojego posta zrozumiałem, że to tylko z moją aplikacją miałeś problemy. Jeśli chodzi o to restartowanie mojej aplikacji, to nie sądziłem, że zaczniesz testować zużycie RAM (przy okazji … czym to testowałeś?) Bo wiesz jak testujesz tylko czas operacji to wystarczy, że podmienisz ścieżkę pliku - nie trzeba za każdym razem otwierać i zamykać całej aplikacji (tutaj przy okazji dodam, że wyniki można skopiować z tego textbox-a - nie trzeba ich przepisywać). Jeśli tego nie zauważyłeś, to napisz. Wprowadzę prędzej, czy później jakiegoś help-a albo coś. Inni potencjalni użytkownicy też mogą tego nie zauważyć.
Nie wiem też jakie różnice w zużyciu RAM są pomiędzy aplikacją okienkową i konsolową - pewno w tym wypadku nieduże, no ale jednak wydaje mi się, że są (oczywiście większe zużycie RAM będzie w przypadku aplikacji okienkowej). Dlatego też podanie precyzyjnych wyników jest po prostu niemożliwe (a z tego co widziałem starałeś się to właśnie osiągnąć). Jeślibyś chociaż napisał w wynikach, że jedna aplikacja jest okienkowa, a druga konsolowa, to w sumie czepiać byś się nie mógł. Tymczasem Ty napisałeś tylko, ze moja aplikacja zawiera przekłamania.
Co do tego automatu … Rozbuduj tą swoją aplikację w wolnej chwili o podawanie parametrów z wiersza poleceń w postaci:
ścieżka pliku | parametry (te wszystkie ignoruj)
Wyniki też muszą mieć wówczas ustaloną strukturę, np:
czas operacji | kolumny z wynikami
Ja też wówczas zrobię taką aplikację konsolową, która będzie przyjmować dokładnie te same parametry i zwracać dokładnie te same wyniki. Mając takie dwie aplikacje konsolowe, które będą przyjmować te same parametry i zwracać wyniki w tej samej postaci, będzie można stosunkowo łatwo napisać taki automat.
Przecież pytałeś się mnie dzisiaj jak poprawiłem wydajność w wersji 1.3. Odpisałem Ci, że m.in. wprowadziłem słowniki zamiast uniwersalnego wykrywania znaków diakrytycznych. To nie jest przecież tylko zmiana w GUI. Jasne, w GUI pojawił się edytor dla słowników, ale to przecież nie było wszystko W bebechach też trzeba było wprowadzić zmiany. W ogóle to nie rozumiem co tutaj próbujesz udowodnić (w sensie do czego zmierzasz)?
A jaka jest wystarczająca?
Chodzi mi o samo to, że explorer szaleje przy tylu plikach zużywając procesor, a więc chyba przy okazji spowalniając Twój program. A co najmniej irytując użytkownika.
IMHO, ze względów wydajnościowych, lepiej wprowadzić zmianę w dwóch miejscach. I to nie jest złe rozwiązanie, a jedynie niezgodne z pierwotną filozofią. Oczywiście nie musimy w ogóle brać wyników tej wersji pod uwagę.
Możesz nie wierzyć, ale to jest po prostu fakt, którego tutaj nie stwierdzimy, bo problem jest w czymś innym, czego obaj do tej pory nie zauważyliśmy. Po prostu, przetwarzanie wierszy to jedno, a przetwarzanie znaków to drugie. Konsekwencją obiektowej struktury mojego programu jest to, że każdy znak wiersza jest przetwarzany w kilku pętlach, przez co mam więcej pętli niż Ty i to one głównie odpowiadają za gorszą wydajność mojego programu.
W tym konkretnym przypadku mój program nie ma prawa być wydajniejszy od Twojego (chociaż aż do Twojej wersji 1.2 był, co jest forma jakiejś magii ;)).
Dziwi mnie jednak inna rzecz - mój FullTransformer działa w zasadzie tak, jak Twój ComplexRowModifier. Czemu zatem mój program jest wolniejszy? Tutaj powinien być remis.
Menedżerem zadań. Ja nie testuję zużycia RAMu, tylko podaję jego zużycie na koniec procesu.
Tak na oko z 5 MB.
Bardziej o orientacyjne mi chodziło.
No bez przesady, przecież wszyscy o tym wiedzą.
Zawiera, bo powoduje odczuwalne nadmierne wykorzystanie RAMu przez system operacyjny. IMHO tak nie powinno być.
Mam to w TODO od jakiegoś czasu, ale nie gwarantuję teraz terminowości wykonania.
O to samo, o co od początku - o łatwość dodawania nowych opcji przetwarzania wierszy, i czytelność kodu po ich dodaniu.
Jeśli będziesz w przyszłości podawał wyniki tej wersji, to zaznaczaj to proszę wyraźnie (tak jak to teraz masz zrobione).
Ja mówiłem dokładnie o tym samym. Wydaje mi się, że im więcej zostanie wybranych parametrów, tym gorsze czasy będziesz uzyskiwał. Przy niedużej liczbie wybranych parametrów (1-2) czasy naszych programów będą zbliżone. Sęk w tym, że to na razie jest tylko moje “wydaje mi się”. Niezbędne jest przeprowadzenie większej liczby testów, żeby to potwierdzić. Testy te muszą obejmować różną ilość parametrów, w różnych kombinacjach.
Oj nie Pierwsze co się rzuca w oczy to to, że metodę ToLower wywołujesz na samym początku metody przetwarzającej - kompletnie niepotrzebnie. Lepiej przenieść wywołanie metody ToLower na koniec metody przetwarzającej. W przypadku niektórych wierszy (tych, w których jakieś znaki zostaną zignorowane) na pewno da to kilka cennych ms. Poza tym możesz zastąpić StringBuilder tablicą char-ów - zerknij jak to wygląda u mnie. Początkowo też miałem StringBuilder, ale użycie tablicy char-ów dało mi kilka ms. A jeśli już chcesz pozostać przy StringBuilder, to może warto rozważyć to, aby uczynić z tej zmiennej zmienną statyczną (StringBuilder ma metodę Clear, która się wówczas przyda). I jeszcze jedno (ale to przetestuj na końcu) … zamiast if (!char.IsDigit© && !char.IsPunctuation© && !char.IsWhiteSpace©) sprawdź, czy szybsza nie będzie wersja tego if-a z jedną negacją i trzema or-ami.
To skoro korzystasz z tego programiku, to może lepiej patrzeć na Szczytowe użycie pamięci?
Mógłbyś napisać w jaki sposób sprawdziłeś to “odczuwalne nadmierne wykorzystanie RAMu”? Ja nie mam pojęcia jak to u siebie odtworzyć. Jeszcze nie ściągałem wprawdzie tego nowego dużego pliku (sprawdzę to dziś po południu), ale patrząc po tym 5x MB wszystko wydaje się być w porządku (mimo tego, że już nie wywołuję ręcznie Flush-a).
Mi się z kolei wydaje, że niekoniecznie, bo jak już pisałem, przyczyną mniejszej wydajności mojego rozwiązania jest wielokrotna analiza tego samego znaku, a nie same warunki logiczne. Czas działania mojego programu zależy praktycznie tylko od czasu działania odpowiednich algorytmów przetwarzających tekst. Przy wybraniu dwóch opcji, końcowy czas działania będzie równy sumie czasu dla jednej i drugiej opcji. Natomiast w przypadku Twojego programu, wszystko zależy od tego, którym warunkiem ifa jest dana opcja. Jeśli pierwszym, to algorytm przejdzie bardzo szybko, jeśli ostatnim, to znacznie wolniej.
Z tego powodu Twój sposób może być wolniejszy od mojego w przypadku wybrania jakiejś skrajnie pesymistycznej dla Twojego ifa opcji, tj. wymagającej sprawdzenia wszystkich warunków. Zatem, im więcej będzie tych warunków, i jeśli testowany będzie ostatni z nich, tym gorsze wyniki osiągnie Twój program.
Słuszna uwaga. Algorytm przyspieszył o prawie 9%.
To zrobiłem zaraz po napisaniu poprzedniego posta w tym wątku, była to jedna z dwóch optymalizacji, o których napomknąłem wcześniej. Dało mi to zwiększenie wydajności o jakieś 5%. A poza tym, właśnie wprowadzając tę zmianę, wpadłem na odkrycie, czemu mój program jest z definicji wolniejszy.
A czemu mógłby być? Kod po kompilacji będzie taki sam.
Odczuwalne oznacza, że je odczułem. Gdy pamięć się kończy, to i kursor myszki nie chodzi tak płynnie, jak powinien.
Gdy Twój program chodzi w trybie HDD, to proces niby zajmuje kilkanaście MB, ale spójrz na wykres historii użycia pamięci fizycznej w Menadżerze Zadań - wyraźnie widać wzrost zużycia, nawet w przypadku tego mniejszego pliku testowego.
Po dłuższej przerwie pojawiła się nowa wersja programu Duplicate Finder v1.4. Tak, wiem, wszyscy nie mogli się doczekać
Najważniejsze zmiany:
Udało mi się podnieść wydajność rozwiązania opartego o większe zużycie RAM (przy większej ilości wybranych parametrów różnica jest prawie dwukrotna).
Pojawił się nowy parametr - Ignoruj symbole.
Możliwe jest przetwarzanie (za jednym zamachem) plików o wybranym formacie umieszczonych w wybranym folderze (oraz jego podfolderach - jeśli ktoś sobie tego zażyczy).
Jeśli chodzi o słowniki, to tutaj nastąpiła rewolucja. Do tej pory było tak, że użytkownik mógł sobie wybrać kilka słowników, które mają zostać użyte w trakcie przetwarzania danego pliku. Doszedłem do wniosku, że ta opcja może prowadzić do problemów, więc ją usunąłem. Teraz jest tak, że użytkownik wybiera z listy język, który przeważa w danym pliku (jeśli w pliku mamy głównie tekst w języku polskim, to należy wybrać język polski). Jeśli dla tego języka utworzony został przez użytkownika słownik znaków diakrytycznych, to w przypadku wybrania opcji Ignoruj znaki diakrytyczne, słownik ten zostanie zastosowany przy przetwarzaniu. Wydaje się pokręcone, ale można się łatwo połapać, co i jak, bo program zwraca dość dokładne informacje (choć dopiero po zakończeniu przetwarzania :P).
Wybór języka decyduje nie tylko o tym jaki słownik znaków diakrytycznych zostanie użyty przy przetwarzaniu pliku(ów), ale także o regułach jakie stosowanie będą w przypadku wybrania opcji Ignoruj wielkość liter.
Przebudowałem interfejs programu - wydaje mi się, że jest teraz bardziej czytelny.
Parametry wprowadzone w programie są zapamiętywane po jego wyłączeniu.
Poprawiłem lokalizację. W niektórych miejscach udało mi się wychwycić parę błędów.
Poprawiłem komunikaty błędów.
Przy pierwszym uruchomieniu, program wykrywa język używany przez SO użytkownika. Jeśli jest to polski, to zostanie wybrany polski. Jeśli jakiś inny, to wybrany zostanie angielski.
Pojawił się przycisk przerwij pozwalający zatrzymać aktualnie trwający proces przetwarzania.
To zasadniczo tyle. Nie udało mi się poprawić wydajności rozwiązania opartego o większe zużycie HDD. Opcja ta na razie jest niedostępna, bo muszę ją jeszcze przemyśleć - być może zacznę stosować jakąś lekką bazę danych zamiast rozwiązania opartego o zwykłe pliki. Jak znajdę czas to się tym zajmę (nic póki co nie obiecuję). Jakby ktoś bardzo tego potrzebował, to proszę dać znać przez priva, albo tutaj na forum.
To co chciałbym jeszcze mieć w tym programie (oprócz oczywiście tego przetwarzania opartego o HDD), to m.in. przetwarzanie plików w innych formatach niż txt i csv, np. pliki word-a i excel-a. Przydałaby się także ikonka Przydałby się także parametr pozwalający wprowadzać w formie wyrażenia regularnego, to co ma być ignorowane przy przetwarzaniu.
I na koniec … Jeśli ktoś znajdzie jakieś błędy, to proszę je zgłaszać tutaj lub przez priv-a. Będę starał się je eliminować możliwie jak najszybciej.
Jeśli chodzi o to zużycie RAM-u, o którym mówiłeś, to się w to nie zagłębiałem (nie znalazłem na to czasu). Na pewno problemem przy rozwiązaniu opartym o większe zużycie HDD jest to, że ja po zakończeniu przetwarzania, próbuję usuwać wszystkie tymczasowe pliki, które utworzyłem (żeby użytkownik nie musiał tego robić za mnie). Przy ilości plików większej niż 100 000, ta operacja trwa wieczność (można łatwo to sprawdzić u siebie), dlatego muszę szukać innego rozwiązania. Poza tym rozwiązanie polegające na tworzeniu tak dużej ilości plików o niewielkiej zawartości (np. kilkadziesiąt bajtów) powoduje, że zużycie dysku twardego wzrasta nieprawdopodobnie (np. 100 000 x domyślny rozmiar klastra, czyli 4 KB).
Jeśli chodzi o Flush(), to wciąż tego nie ma w moim kodzie. IMO jest to niepotrzebne. Niech bufor sam się oczyszcza, gdy się zapełni. Prawdę powiedziawszy zwiększyłem jego rozmiar (podniosło to trochę wydajność). Aktualnie jest zrobione tak, że rozmiar bufora można konfigurować z poziomu plików konfiguracyjnych.
Odkopałem temat jak widzę po wielu latach. Ale mam mały problem.
Program przy usuwaniu duplikatów usuwa puste linie a tego bym nie chciał.
Czy jest możliwość by program nie usuwał pustych linii przy usuwaniu duplikatów ? Może jakaś opcja w ustawieniach ?
Jestem bardzo ciekawy czy w ogóle ktoś zareaguje na mój post po takiej długiej przerwie
Chyba najprościej byłoby rozwiązać ten problem z użyciem awk
. Program ten można pozyskać pod systemami Windows na wiele sposobów:
Do wyboru, do koloru.
Przejdźmy zatem do konkretów. Polecenie usuwające zduplikowane linie mogłoby wyglądać tak:
awk '!a[$0]++'
Jest to skrócony zapis następującego polecenia:
awk '!a[$0]++ { print $0 }'
Całość opiera się na tablicy asocjacyjnej (a
), gdzie zawartość linii ($0
) jest kluczem. Początkowa wartość danego elementu tablicy (a[$0]
) będzie pustym napisem. Wartość zanegowanego wyrażenie zwróci true
, co będzie skutkowało wypisaniem zawartości linii. Jednocześnie wartość elementu za każdym razem ulega inkrementacji (pusty napis jest konwertowany na zero), dzięki czemu niejako oznaczamy go jako zapamiętany.
Stwórzmy sobie jakieś przykładowe dane wejściowe:
$ echo -e '1\n2\n3\n\n1\n2\n3\n\n7\n2\n9\n\na\nb\nc'
1
2
3
1
2
3
7
2
9
a
b
c
W rezultacie otrzymujemy:
$ echo -e '1\n2\n3\n\n1\n2\n3\n\n7\n2\n9\n\na\nb\nc' | awk '!a[$0]++'
1
2
3
7
9
a
b
c
Jeśli chcielibyśmy zachować wszystkie puste linie, to moglibyśmy posłużyć się czymś takim:
awk '!a[$0]++ { print $0; next } /^[[:space:]]*$/ { print $0 }'
Na dobrą sprawę $0
przy wypisywaniu danych można pominąć w zapisie, ale zachowałem to dla większej czytelności:
awk '!a[$0]++ { print; next } /^[[:space:]]*$/ { print }'
W tym przypadku jeśli trafimy na duplikat, ale okaże się on pustą linią (zdefiniowaną jako składającą się co najwyżej z samych białych znaków: /^[[:space:]]*$/
), to również wypisujemy go na wyjściu.
Odnosząc się jeszcze raz do naszego przykładu, w wyniku otrzymujemy:
$ echo -e '1\n2\n3\n\n1\n2\n3\n\n7\n2\n9\n\na\nb\nc' | awk '!a[$0]++ { print $0; next } /^[[:space:]]*$/ { print $0 }'
1
2
3
7
9
a
b
c
Dwie puste linie obok siebie to nie błąd, a działanie zgodne z oczekiwaniami. Nie było wszak mowy o ich redukcji. Gdyby jednak nam na tym zależało, to najprostszym rozwiązaniem byłoby użycie cat -s
:
$ echo -e '1\n2\n3\n\n1\n2\n3\n\n7\n2\n9\n\na\nb\nc' | awk '!a[$0]++ { print $0; next } /^[[:space:]]*$/ { print $0 }' | cat -s
1
2
3
7
9
a
b
c
Tu jednak jest pewien haczyk. Jeśli linie nie byłyby dosłownie puste, a zawierały jakieś białe znaki (np. spacje czy tabulatory), to ta sztuczka mogłaby się nie udać.
$ echo -e '1\n2\n3\n\n1\n2\n3\n \n7\n2\n9\n\na\nb\nc' | awk '!a[$0]++ { print $0; next } /^[[:space:]]*$/ { print $0 }' | cat -s
1
2
3
7
9
a
b
c
I na to znajdzie się jednak rada:
awk '/^[[:space:]]*$/{ if (!b++) print; next } { b=0; print }'
Jeśli mamy pustą linię, to przekazujemy ją na wyście tylko wtedy, jeśli poprzednia nie była pusta.
Ostatecznie wychodzi nam coś takiego:
$ echo -e '1\n2\n3\n\n1\n2\n3\n \n7\n2\n9\n\na\nb\nc' | awk '!a[$0]++ { print $0; next } /^[[:space:]]*$/ { print $0 }' | awk '/^[[:space:]]*$/{ if (!b++) print; next } { b=0; print }'
1
2
3
7
9
a
b
c
Jeśli zaś chcielibyśmy zastąpić duplikaty pustymi liniami, to znajdzie się prostszy sposób:
awk '!a[$0]++ { print $0; next } { print "" }'
W takim wypadku otrzymujemy coś takiego:
$ echo -e '1\n2\n3\n\n1\n2\n3\n \n7\n2\n9\n\na\nb\nc' | awk '!a[$0]++ { print $0; next } { print "" }'
1
2
3
7
9
a
b
c
Jak widać, możliwości dostosowania tego pod siebie są olbrzymie. Należy tylko dokładnie wiedzieć czego się chce.