emulator '51: podzial na watki i operacje bitowe

0

Ja posiadam komputer stacjonarny z Pentium IV z HT, a kolega posiada laptop z AMD Athlon x2 (dwa rdzenie). Oboje posiadamy Windows XP i konto administratora.

Program działa tak, że pracuje pętla wykonawcza i w regularnych odstępach czasu lub co określona ilość przebiegów pętli wywoływana jest procedura wyświetlająca.

Miernikiem wydajności programu jest ilość iteracji na sekundę i dążę do zwiększenia tej wartości.

Oprócz dopracowywania metod wywoływanych w głównej pętli, wydajność powinna się zwiększyć poprzez uruchamianie procedury wyświetlania w osobnym wątku. Widać to na obrazku:

user image
Czerwony prostokąt - Jedna iteracja pętli
Zielony prostokąt - Procedura wyświetlania danych o stanie programu
Niebieska strzałka - Moment wywołania procedury wyświetlającej, w opcjach można wybrać, czy co określoną ilość iteracji, czy w regularnych odstępach czasu (wywoływanie z timera położonego na głównej formie)

W pionie z góry na dół jest upływ czasu, wymiary czerwonych i zielonych prostokątów nie obrazują proporcji rzeczywistych czasów wykonywania, rysunek ma charakter poglądowy.

Chodzi mi o napisanie aplikacji wielowątkowej i sens stosowania wielowątkowości.

Ponieważ kolega nie ma zainstalowanego VisualStudio, przesyłałem mu gotowe pliki EXE. Ja piszę ten program w VS2005.

Kolega miał dwa pliki EXE, które różnią się tylko tym, że w jednym wykorzystuje się wielowątkowość, a w drugim jej nie ma. Cała różnica jest w jednym miejscu kodu źródłowego.

W wersji jednowątkowej:

private void UpdateDisplay()
{
    if (this.DispIdle)
    {
        try
        {
            this.UpdateDisplayWork();
        }
        catch
        {
            this.DispIdle = true;
        }
    }
}

W wersji wielowątkowej:

private void UpdateDisplay()
{
    if (this.DispIdle)
    {
        try
        {
            Thread thread = new Thread(new ThreadStart(this.UpdateDisplayWork));
            thread.Priority = ThreadPriority.Normal;
            thread.Start();
        }
        catch
        {
            this.DispIdle = true;
        }
    }
}

Procedura UpdateDisplayWork:

private void UpdateDisplayWork()
{
    if (this.DispIdle)
    {
        this.DispIdle = false;

        // Tu jest treść metody, która ma być wykonywana w osobnym wątku

        this.DispIdle = true;
    }
}

Timer bądź pętla główna programu uruchamia procedurę UpdateDisplay(), która z założenia ma być jak najprostsza, ale jej zadaniem jest uruchomienie właściwej procedury UpdateDisplayWork(), która ma być odpalana w osobnym wątku.

Przeprowadziłem eksperyment u siebie i u kolegi. Według wcześniejszych testów (np, w wersji jednowątkowej w przedstawionym fragmencie zamienię instrukcję "this.UpdateDisplayWork();" na komentarz przez dopisanie "//"), teoretycznie, jeżeli udałoby się przejść z modelu przedstawionego na rysunku zatytułowanego "Jeden wątek" na model zatytułowany "Dwa wątki", to różnica powinna być bardzo widoczna.

W praktyce, na moim komputerze nie widzę żadnej różnicy wydajności, bez względu na to, którą wersję programu odpalam. Mimo wszystko to mnie nie dziwi, bo P4 to jest tak naprawdę jeden rdzeń, a ten przereklamowany HyperThreading ponoć tylko nieznacznie zwiększa wydajność, a do tego programy muszą być odpowiednio napisane. Natomiast u kolegi, który posiada procesor dwurdzeniowy można było się spodziewać różnicy, bo tam są naprawdę dwa rdzenie, które pozwalają zrównoleglić pracę wątków.

W praktyce, kolega też nie widział różnicy. Ponadto obserwował zużycie rdzenie w menedżerze zadań i twierdzi, że ten program, zarówno w jednej, jak i w drugiej wersji, wykorzystuje tylko jeden rdzeń w 100%, a drugiego wcale.

Ponadto, w przypadku wersji wielowątkowej, w przypadku ustawienia odświeżania co określona ilość iteracji, przy każdej próbie wystartowania pojawia się taki błąd:

See the end of this message for details on invoking
just-in-time (JIT) debugging instead of this dialog box.

         * Exception Text **************

System.InvalidOperationException: Object is currently in use elsewhere.
at System.Drawing.Graphics.CheckErrorStatus(Int32 status)
at System.Drawing.Graphics.DrawImage(Image image, Int32 x, Int32 y, Int32 width, Int32 height)
at System.Drawing.Graphics.DrawImage(Image image, Rectangle rect)
at System.Windows.Forms.PictureBox.OnPaint(PaintEventArgs pe)
at System.Windows.Forms.Control.PaintWithErrorHandling(PaintEventArgs e, Int16 layer, Boolean disposeEventArgs)
at System.Windows.Forms.Control.WmPaint(Message& m)
at System.Windows.Forms.Control.WndProc(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.OnMessage(Message& m)
at System.Windows.Forms.Control.ControlNativeWindow.WndProc(Message& m)
at System.Windows.Forms.NativeWindow.Callback(IntPtr hWnd, Int32 msg, IntPtr wparam, IntPtr lparam)

         * Loaded Assemblies **************

mscorlib
Assembly Version: 2.0.0.0
Win32 Version: 2.0.50727.3082 (QFE.050727-3000)
CodeBase: file:///C:/WINDOWS/Microsoft.NET/Framework/v2.0.50727/mscorlib.dll

DSM-51
Assembly Version: 1.0.0.0
Win32 Version: 1.0.0.0
CodeBase: file:///D:/download/andrew/Debug/DwaWatki.exe

System.Windows.Forms
Assembly Version: 2.0.0.0
Win32 Version: 2.0.50727.3053 (netfxsp.050727-3000)
CodeBase: file:///C:/WINDOWS/assembly/GAC_MSIL/System.Windows.Forms/2.0.0.0__b77a5c561934e089/System.Windows.Forms.dll

System
Assembly Version: 2.0.0.0
Win32 Version: 2.0.50727.3053 (netfxsp.050727-3000)
CodeBase: file:///C:/WINDOWS/assembly/GAC_MSIL/System/2.0.0.0__b77a5c561934e089/System.dll

System.Drawing
Assembly Version: 2.0.0.0
Win32 Version: 2.0.507

Co dziwne, ten sam program u mnie działa bez żadnych problemów.

Czy przedstawiony sposób odpalania drugiego wątku w przypadku wersji wielowątkowej, jest prawidłowy? Jeżeli nie, to jak to powinno wyglądać, żeby system wykonywał drugi wątek na drugim rdzeniu?

Jaka może być przyczyna, że ten sam program u kolegi wywala błąd, a u mnie działa, mimo, że oboje mamy Windows XP, a program jest z Visual Studio 2005?

0

Wszystko bierze się z jednego błędu: otóż GUI może być obsługiwane przez tylko jeden wątek. A Ty próbujesz zmusić windowsa, żeby GUI chodziło w Twoim wątku, a w wątku GUI wykonujesz pracę, do której ten wątek nie został stworzony. W aplikacji konsolowej, to co opisujesz, działałoby tak jak tego oczekujesz (możesz spróbować przenieść kod na konsolę i zobaczyć jak wtedy będzie to działało). Lecz w aplikacji okienkowej działają zupełnie inne mechanizmy i wymagają one innego podejścia do tematu.

Jeśli chcesz przystosować to pod aplikację okienkową, utwórz oddzielny wątek dla Twojej pętli i z niego wywołuj metodę aktualizującą GUI za pomocą BeginInvoke() lub przy użyciu metody Post() kontekstu synchronizacji (vide -> temat "Wywolywanie eventu tak, by obslugiwal go MainThread" na tym forum).

0

Przedstawiam procedurę RegDisp, która jest jedną z procedur wywoływanych w procedurze UpdateDisplayWork:

        // Delegacja konieczna przy pracy wielowatkowej
        private delegate void Del();

        // Wyswietlanie stanu rejestrow
        private void RegDisp()
        {
            PanelRegs.Invoke((Del)delegate
            {
                RegPC.Text = IntToHex16(PC);
                RegSP.Text = IntToHex8(SP);
                RegDPTR.Text = IntToHex8(DPH) + IntToHex8(DPL);
                RegA.Text = IntToHex8(BitRegRead(224));
                RegB.Text = IntToHex8(BitRegRead(240));
                RegP0.Text = IntToBin8(PRead(0));
                RegP1.Text = IntToBin8(PRead(1));
                RegP2.Text = IntToBin8(PRead(2));
                RegP3.Text = IntToBin8(PRead(3));
                RegPSW.Text = IntToBin8(BitRegRead(208));
                RegIE.Text = IntToBin8(BitRegRead(168));
                RegIP.Text = IntToBin8(BitRegRead(184));
                RegTCON.Text = IntToBin8(BitRegRead(136));
                RegTMOD.Text = IntToBin8(TMOD);
                RegPCON.Text = IntToBin8(PCON);
                RegSCON.Text = IntToBin8(BitRegRead(152));
                RegT0H.Text = IntToHex8(TH0);
                RegT0L.Text = IntToHex8(TL0);
                RegT1H.Text = IntToHex8(TH1);
                RegT1L.Text = IntToHex8(TL1);
            });
        }

Jak widać, tutaj zastosowałem aktualizację pól tekstowych przez delegację, bo bez delegacji to nie działa (wywala błąd), natomiast pozostałe elementu GUI (jeden DataGrid, suwak TrackBar i kilka obrazków) są sterowane bezpośrednio (w przypadku tych kontrolek to działa).

Później poczytam ten tekst i spróbuję dać główną pętlę do wątku pomocniczego, może coś z tego wyjdzie.

Ale zanim się do tego wezmę, to mam takie pytanko: Czy jak zrobię obsługę i odwołania do wszystkich elementów GUI poprzez delegację, podobnie, jak powyżej (mimo, że bezpośrednie odwołanie też działa), to czy osiągnę zamierzony cel, czy to jest bez znaczenia w tej sprawie?

0

Na wstępie pytanko, jeśli oczywiście można wiedzieć, piszesz jakiś symulator '51?

Jak widać, tutaj zastosowałem aktualizację pól tekstowych przez delegację, bo bez delegacji to nie działa (wywala błąd)
Oczywiście.

natomiast pozostałe elementu GUI (jeden DataGrid, suwak TrackBar i kilka obrazków) są sterowane bezpośrednio (w przypadku tych kontrolek to działa).
Działa, ale nie znaczy, że zawsze będzie działać. Te kontrolki musisz tak samo przez delegata i invoke kontrolować. Zresztą błąd, o którym piszesz w swoim poprzednim poście jest dowodem, że nie zawsze to działa.
No ale dopóki będziesz używał Invoke() i jednocześnie wykonywał pętlę w głównym wątku, to zupełnie nic nie zyskasz na wydajności. Invoke() ma to do siebie, że zatrzymuje wątek, w którym jest wywoływane, przesyła do głównego wątku wiadomość, żeby ten wykonał danego delegata, wątek główny wykonuje delegata, po zakończeniu wykonywania delegata, wątek, który wywołał Invoke(), jest zwalniany. Czyli delegat, który podajesz w parametrze Invoke() jest wykonywany faktycznie w tym samym wątku co pętla.

Żeby działało to w lepiej, musisz pętlę puścić koniecznie w oddzielnym wątku. I w tym nowym wątku wywoływać metodę BeginInvoke(), żeby nie czekał on na zakończenie wykonywania delegata lub ewentualnie metodę Post() kontekstu synchronizacji (co jest opisane w poście do którego dałem Ci link). Lepiej nie próbuj tworzyć kolejnych wątków z wątku w którym wykonuje się pętla, bo tworzenie i uruchamianie wątku zabierze sporo czasu, a i tak wywołanie BeginInvoke() lub Post() może odbyć się na tym samym procesorze lub rdzeniu, na którym działa pętla.

0
  1. Inicjowanie odmalowania niezależnie przez wiele wątków nie będzie wydajne. Wywołania będą się zazębiać i nie będą w równych odstępach. Dlatego tutaj należałoby zastosować stały odstęp czasowy. Częstotliwość odświeżania można by kontrolować suwakiem/ustawieniem, im mniejsza tym więcej czasu dla wątków roboczych.
    Odmalowanie (zmiana atrybutów kontrolek) musi się odbywać w wątku głównym dlatego właśnie tam można by umieścić czasomierz vide System.Windows.Forms.Timer. Dokładniejszy, ale za to bardziej wymagający byłby timer działający w osobnym wątku (System.Threading.Timer lub System.Timers.Timer lub petla ze Sleep'em w osobnym wątku) korzystający z Invoke / BeginInvoke.

I teraz pytanie - czy wyświetlana klatka musi zawierać stan rejestrów z pomiędzy kompletnych kroków obliczeniowych, czy może być mieszanką z kilku kolejnych kroków. Jeśli odświeżanie będzie miało dobrą częstotliwość to ludzkiemu oku nie będzie robiło różnicy, czy stany rejestrów mieszają się. Dopiero kiedy będziemy w stanie uchwycić wzrokiem konkretną klatkę może mieć to znaczenie (chodź nie musi, to jest twoje widzi-mi-się).
Wybór ma taką wagę, że decyduje o wielkości strat poniesionych na synchronizacji. Jeśli wybrać by opcję drugą to straty będą mniejsze. Możliwe nawet, że synchronizacja nie będzie w ogóle potrzebna, zależy, czy metody BitRegRead oraz PRead mogą być bezpiecznie wywoływane z wątku głównego podczas gdy wątki robocze pracują na rejestrach i zmieniają ich stany.

Jakbyś pokazał więcej kodu było by miło. Możesz nawet większe fragmenty wrzucić tu: http://pastebin.4programmers.net . Szczególnie chodzi o metody BitRegRead, PRead oraz procedurę wątku roboczego.


private void UpdateDisplay()
{
if (this.DispIdle)
{
try
{
Thread thread = new Thread(new ThreadStart(this.UpdateDisplayWork));
thread.Priority = ThreadPriority.Normal;
thread.Start();
}
catch
{
this.DispIdle = true;
}
}
}
//...
private void UpdateDisplayWork()
{
if (this.DispIdle)
{
this.DispIdle = false;

    // Tu jest treść metody, która ma być wykonywana w osobnym wątku

    this.DispIdle = true;
}

}

Powyższy mechanizm nie gwarantuje wyłączności wykonywania operacji najwyżej w jednym wątku! Podstawowy błąd niezastosowania mechanizmów synchronizacji. Aby osiągnąć zamierzany przez ciebie efekt trzeba by testować wartość zmiennej <i>DispIdle</i> i jednocześnie zmieniać jej wartość (musi to być operacja atomowa) przy pomocy <i>Interlocked.CompareExchange</i> lub zamiast tej zmiennej użyć <i>AutoResetEvent</i>:
```csharp
//1. 
int DispIdle = 1;
private void UpdateDisplay()
{
    if (Interlocked.CompareExchange(ref DispIdle, 0, 1)
    {
        try
        {
            Thread thread = new Thread(new ThreadStart(this.UpdateDisplayWork));
            thread.Priority = ThreadPriority.Normal;
            thread.Start();
        }
        finally
        {
            this.DispIdle = 1;
        }
    }
}
//...
private void UpdateDisplayWork()
{
    // Tu jest treść metody, która ma być wykonywana w osobnym wątku
}
//2.
AutoResetEvent DispIdle = new AutoResetEvent(true);
private void UpdateDisplay()
{
    if (this.DispIdle.WaitOne(0, false))
    {
        try
        {
            Thread thread = new Thread(new ThreadStart(this.UpdateDisplayWork));
            thread.Priority = ThreadPriority.Normal;
            thread.Start();
        }
        finally
        {
            this.DispIdle.Set()
        }
    }
}
//...
private void UpdateDisplayWork()
{
    // Tu jest treść metody, która ma być wykonywana w osobnym wątku
}

  1. Błąd o którym pisałeś jest spowodowany najprawdopodobniej przez odwołanie się do PictureBox'a lub jego obrazka spoza wątku głównego.
0
gufiak napisał(a)

Na wstępie pytanko, jeśli oczywiście można wiedzieć, piszesz jakiś symulator '51?

Zgadłeś. A dokładnie to piszę emulator systemu DSM-51 jako inżynierkę.

Co do obrazków (kontrolek PictureBox), to jedyna zastosowana metoda to podmiana obrazków na obrazek z pamięci poprzez "NazwaKontrolki.Image = ZmiennaZObrazkiem". Obrazki nie podlegają żadnym zmianom.

PitRegRead i PRead:
http://pastebin.4programmers.net/339

Procedura robocza:
http://pastebin.4programmers.net/340

Oczywiście, wszystkie fragmenty wyjaśnię, jak będzie potrzeba.

Co do odświeżania stanów rejestrów, to można to wykonywać na dwa sposoby:

  • Co określoną liczbę rozkazów
  • Co określony czas

Co do synchronizacji i "atomowości", to ja rozumiem, że to jest istotne np. w takiej sytuacji:
Wątek 1:
a = 3;
b = 4;
c = 5;
d = a * c;
e = d + b;
Wątek 2:
b = 2;
d = 8;
a = b * d;
c = a - b;
e = a / c;

i założyć by, że zmienne a,b,c,d,e są typu int i zdefiniowane globalnie w ramach całej klasy.

Czy to jest prawda, że jakby odpalić te dwa wątki naraz to w praktyce otrzymamy działania równoważne temu lub w nieco innej kolejności:
a = 3;
b = 2;
b = 4;
d = 8;
c = 5;
a = b * d;
d = a * c;
c = a - b;
e = d + b;
e = a / c;
i to spowoduje, że zmienna "e" w obu wątkach dostanie błędną wartość. Czy dobrze myślę?

0

Fajne, ale musisz poprawić to o czym była mowa w tym temacie.
Pobieżnie rzuciłem okiem na Twój kod i mam taką uwagę:

if (PC > 65535)
{
    PC = PC - 65536;
}
if (PC < 0)
{
    PC = PC + 65536;
}

ushort PC; rozwiąże sprawę z automatu, zawsze kilka cykli odpadnie z pętli.

Kolejna sprawa, z tego co widzę dla RegBits zastosowałeś typ bool. Zyskałbyś dużo na wydajności przechowując rejestry w postaci liczby typu byte, albo jeszcze lepiej, jakbyś operował na typie int na 8 najmłodszych bitach. Operacje na kilku bool-ach są dużo wolniejsze niż operacje na jednym bajcie, a na procesorach 32 bitowych najszybsze są operacje na typie int. Do tego konwersja z bool do int jaką zastosowałeś jest dosyć wolna. Więc zamiast tak:

        // Odczyt rejestru z adresowaniem bitowym
        private int BitRegRead(int Addr)
        {
            int B7 = Convert.ToInt16(RegBits[Addr + 7]) * 128;
            int B6 = Convert.ToInt16(RegBits[Addr + 6]) * 64;
            int B5 = Convert.ToInt16(RegBits[Addr + 5]) * 32;
            int B4 = Convert.ToInt16(RegBits[Addr + 4]) * 16;
            int B3 = Convert.ToInt16(RegBits[Addr + 3]) * 8;
            int B2 = Convert.ToInt16(RegBits[Addr + 2]) * 4;
            int B1 = Convert.ToInt16(RegBits[Addr + 1]) * 2;
            int B0 = Convert.ToInt16(RegBits[Addr]);
            return B7 + B6 + B5 + B4 + B3 + B2 + B1 + B0;
        }

Lepiej zrobić tak:

		private int BitRegRead(int Addr)
		{
			int ret = RegBits[Addr + 7] ? 128 : 0;
			ret |= RegBits[Addr + 6] ? 64 : 0;
			ret |= RegBits[Addr + 5] ? 32 : 0;
			ret |= RegBits[Addr + 4] ? 16 : 0;
			ret |= RegBits[Addr + 3] ? 8 : 0;
			ret |= RegBits[Addr + 2] ? 4 : 0;
			ret |= RegBits[Addr + 1] ? 2 : 0;
			ret |= RegBits[Addr] ? 1 : 0;
			return ret;
		}

Niemniej jednak, zamiast bool lepiej używaj typu int, niewątpliwie zauważysz różnicę.

Czy to jest prawda, że jakby odpalić te dwa wątki naraz to w praktyce otrzymamy działania równoważne temu lub w nieco innej kolejnośc
Mniej więcej o to chodzi. Może nie będzie aż tak drastycznie, bo context switch nie następuje przy każdej operacji, ale zawsze jest prawdopodobieństwo, że nastąpi on w miejscu, gdzie może to spowodować problemy.

0

Natomiast BigRegWrite oraz procedury konwertujące między postacią dziesiętną, a szeregiem 8 bool wygląda tak:

        // Zapis rejestru z adresowaniem bitowym
        private void BitRegWrite(int Addr, int Data)
        {
            bool B7;
            bool B6;
            bool B5;
            bool B4;
            bool B3;
            bool B2;
            bool B1;
            bool B0;
            IntToBin(Data, out B7, out B6, out B5, out B4, out B3, out B2, out B1, out B0);
            RegBits[Addr + 7] = B7;
            RegBits[Addr + 6] = B6;
            RegBits[Addr + 5] = B5;
            RegBits[Addr + 4] = B4;
            RegBits[Addr + 3] = B3;
            RegBits[Addr + 2] = B2;
            RegBits[Addr + 1] = B1;
            RegBits[Addr] = B0;
        }

        // Zamiana liczby na reprezentacje bitowa
        private void IntToBin(int N0, out bool B7, out bool B6, out bool B5, out bool B4, out bool B3, out bool B2, out bool B1, out bool B0)
        {
            int N = N0;
            if (N >= 128)
            {
                B7 = true;
                N = N - 128;
            }
            else
            {
                B7 = false;
            }
            if (N >= 64)
            {
                B6 = true;
                N = N - 64;
            }
            else
            {
                B6 = false;
            }
            if (N >= 32)
            {
                B5 = true;
                N = N - 32;
            }
            else
            {
                B5 = false;
            }
            if (N >= 16)
            {
                B4 = true;
                N = N - 16;
            }
            else
            {
                B4 = false;
            }
            if (N >= 8)
            {
                B3 = true;
                N = N - 8;
            }
            else
            {
                B3 = false;
            }
            if (N >= 4)
            {
                B2 = true;
                N = N - 4;
            }
            else
            {
                B2 = false;
            }
            if (N >= 2)
            {
                B1 = true;
                N = N - 2;
            }
            else
            {
                B1 = false;
            }
            if (N >= 1)
            {
                B0 = true;
            }
            else
            {
                B0 = false;
            }
        }

        // Zamiana reprezentacji bitowej na liczbe
        public int BinToInt(bool B7, bool B6, bool B5, bool B4, bool B3, bool B2, bool B1, bool B0)
        {
            int NB7;
            int NB6;
            int NB5;
            int NB4;
            int NB3;
            int NB2;
            int NB1;
            int NB0;
            if (B7)
            {
                NB7 = 128;
            }
            else
            {
                NB7 = 0;
            }
            if (B6)
            {
                NB6 = 64;
            }
            else
            {
                NB6 = 0;
            }
            if (B5)
            {
                NB5 = 32;
            }
            else
            {
                NB5 = 0;
            }
            if (B4)
            {
                NB4 = 16;
            }
            else
            {
                NB4 = 0;
            }
            if (B3)
            {
                NB3 = 8;
            }
            else
            {
                NB3 = 0;
            }
            if (B2)
            {
                NB2 = 4;
            }
            else
            {
                NB2 = 0;
            }
            if (B1)
            {
                NB1 = 2;
            }
            else
            {
                NB1 = 0;
            }
            if (B0)
            {
                NB0 = 1;
            }
            else
            {
                NB0 = 0;
            }
            return NB7 + NB6 + NB5 + NB4 + NB3 + NB2 + NB1 + NB0;
        }

Analogicznie już opracowałem odpowiednią wersję procedury BinToInt:

        // Zamiana reprezentacji bitowej na liczbe
        public int BinToInt(bool B7, bool B6, bool B5, bool B4, bool B3, bool B2, bool B1, bool B0)
        {
            int ret = B7 ? 128 : 0;
            ret |= B6 ? 64 : 0;
            ret |= B5 ? 31 : 0;
            ret |= B4 ? 16 : 0;
            ret |= B3 ? 8 : 0;
            ret |= B2 ? 4 : 0;
            ret |= B1 ? 2 : 0;
            ret |= B0 ? 1 : 0;
            return ret;
        }

Podobnie mysląc, próbuję opracować lepszą wersją IntToBin

        private void IntToBin(int N0, out bool B7, out bool B6, out bool B5, out bool B4, out bool B3, out bool B2, out bool B1, out bool B0)
        {
            B7 = Convert.ToBoolean(N0 >> 7 & 1);
            B6 = Convert.ToBoolean(N0 >> 6 & 1);
            B5 = Convert.ToBoolean(N0 >> 5 & 1);
            B4 = Convert.ToBoolean(N0 >> 4 & 1);
            B3 = Convert.ToBoolean(N0 >> 3 & 1);
            B2 = Convert.ToBoolean(N0 >> 2 & 1);
            B1 = Convert.ToBoolean(N0 >> 1 & 1);
            B0 = Convert.ToBoolean(N0 >> 0 & 1);
        }

Czy jest dobrze, czy można jeszcze lepiej?

0
  1. Jeśli zależy Ci na wydajności programu, unikaj Convert jak ognia. W ogóle minimalizacja ilości wywoływanych funkcji w często powtarzanych operacjach jest również bardzo ważna.

  2. Aż się prosi, żeby umieścić te bool-e w strukturze. Różnica jest duża, gdyż w taki sposób jak Ty przekazujesz parametry, każdy oddzielny parametr musi zostać odłożony na stos. Tak to wygląda w assemblerze:

			IntToBin(Data, out B7, out B6, out B5, out B4, out B3, out B2, out B1, out B0);
0000002c  lea         eax,[ebp-18h]        <- tu zaczynamy zrzucać parametry na stos
0000002f  push        eax  
00000030  lea         eax,[ebp-1Ch] 
00000033  push        eax  
00000034  lea         eax,[ebp-20h] 
00000037  push        eax  
00000038  lea         eax,[ebp-24h] 
0000003b  push        eax  
0000003c  lea         eax,[ebp-28h] 
0000003f  push        eax  
00000040  lea         eax,[ebp-2Ch] 
00000043  push        eax  
00000044  lea         eax,[ebp-30h] 
00000047  push        eax  
00000048  lea         edx,[ebp-14h] 
0000004b  mov         ecx,dword ptr [ebp-10h] 
0000004e  call        dword ptr ds:[00A00AB8h]     <- a dopiero tu wywołujemy funckję

Jeśli natomiast przekazujesz strukturę, to kod wygląda następująco:

			RegByte rb = IntToBin(Data);
00000024  lea         ecx,[ebp-20h]
00000027  mov         edx,dword ptr [ebp-10h] 
0000002a  call        dword ptr ds:[00950AB8h]    <- już po dwóch instrukcjach następuje wywołanie funkcji
00000030  lea         edi,[ebp-18h] 
00000033  lea         esi,[ebp-20h] 
00000036  movq        xmm0,mmword ptr [esi] 
0000003a  movq        mmword ptr [edi],xmm0  <- a te 4 instrukcje dodatkowo pojawiły się w związku z użyciem struktury

Czyli zamiast 16 instrukcji, mamy 6. Oczywiście, na innym procesorze może to wyglądać inaczej. Ale przy tylu parametrach dobrym nawykiem jest używanie struktur. A tak wygląda kod, z użyciem struktury:

public struct RegByte
{
	public bool B7;
	public bool B6;
	public bool B5;
	public bool B4;
	public bool B3;
	public bool B2;
	public bool B1;
	public bool B0;
}
// Zapis rejestru z adresowaniem bitowym
public static void BitRegWrite(int Addr, int Data)
{
	RegByte rb = IntToBin(Data);
	RegBits[Addr + 7] = rb.B7;
	RegBits[Addr + 6] = rb.B6;
	RegBits[Addr + 5] = rb.B5;
	RegBits[Addr + 4] = rb.B4;
	RegBits[Addr + 3] = rb.B3;
	RegBits[Addr + 2] = rb.B2;
	RegBits[Addr + 1] = rb.B1;
	RegBits[Addr] = rb.B0;
}

public static RegByte IntToBin(int N0)
{
	return new RegByte
	{
		B7 = (N0 & 0x80) != 0,
		B6 = (N0 & 0x40) != 0,
		B5 = (N0 & 0x20) != 0,
		B4 = (N0 & 0x10) != 0,
		B3 = (N0 & 0x08) != 0,
		B2 = (N0 & 0x04) != 0,
		B1 = (N0 & 0x02) != 0,
		B0 = (N0 & 0x01) != 0
	};
}
  1. Ale na prawdę dużo lepiej jest darować sobie te zabawy z typem bool i przejść na liczby typu byte lub int. Zobaczysz, że dużo operacji będzie można pominąć i zwyczajnie operować na bitach operatorami bitowymi oraz logicznymi. Każdy procesor ma instrukcje logiczne i są one dużo szybciej wykonywane niż konwersje z int-a do kilku bool-i czy na odwrót. A przy okazji program będzie używał mniej pamięci, mniej elementów będzie miał do adresowania, zacznie działać dużo szybciej.
0

A BitArray/BitVector32 nie znajdą tu zastosowania?

0

somekind, te typy istnieją dla wygody programisty, a nie szybkości działania. Żeby działało szybko, najlepiej używać tylko najprostszych typów i wykonywać na nich jak najmniej operacji.

0

W ogóle źle, że każdy bit trzymasz w osobnym bajcie. Źle.
Tracisz na konwersji pomiędzy bit<->bajt.
Tracisz na operacjach wielobitowych - zamiast wykonać jedną na ośmiu bitach na raz musisz na każdym bicie wykonać ją osobno.
Ogólnie tylko tracisz.
Trzymaj bity po 8 w jednym bajcie. Można sobie ułatić dostęp do konkretnych bitów takim rozszerzeniem:

public static class ByteBitExt
{
    public static bool GetBit(this Byte b, int n) { return  0 != (b &  (1 << n)); }
    public static byte SetBit(this Byte b, int n) { return (byte)(b |  (1 << n)); }
    public static byte ClrBit(this Byte b, int n) { return (byte)(b & ~(1 << n)); }
    public static byte TglBit(this Byte b, int n) { return (byte)(b ^  (1 << n)); }
}
//... przykładowo:
byte b = 2;
b = b.SetBit(5);

I patrz jak się teraz wszystko upraszcza:

//IntToBin - niepotrzebne
//BinToInt - niepotrzebne

byte RegBits[] = new byte[(ILOSC_POTRZEBNYCH_BITOW + 15) / 8]; //dopełniamy do pełnych ośmiu bitów oraz dodajemy jeden bajt zapasu aby uprościć operacje odczytu i zapisu

// Zapis rejestru z adresowaniem bitowym
public void BitRegWrite(uint Addr, byte Data)
{
    uint offset = Addr & 7; //szybsze "Addr % 8;"
    Addr >>= 3; //szybsze "Addr /= 8";
    uint mask = 0xFF << offset;
    RegBits[Addr] = (byte)(RegBits[Addr] & ~mask | (Data << offset));
    RegBits[Addr + 1] = (byte)(RegBits[Addr + 1] & mask | (Data >> (8 - offset)));
}

private byte BitRegRead(uint Addr)
{
    uint offset = Addr & 7; //szybsze "Addr % 8;"
    Addr >>= 3; //szybsze "Addr /= 8";
    return (byte)((RegBits[Addr] >> offset) | (RegBits[Addr + 1] << (8 - offset)));
}

byte[] Ports = new byte[4];
// Odczyt od wewnatrz z portu
private byte PRead(int Port)
{
    return Ports[Port] & RegBits[16 + 2 * Port];
}

Metody odczytu (BitRegRead, PRead) możesz wywoływać bez synchronizacji z wątku głównego. Ale wynik może być mieszanką z kilku kolejnych kroków.
Aby mieć dokładny stan spomiędzy kroków trzeba zsynchronizować odczyt z wykonywaniem kroków.

andrzejlisek napisał(a)

Co do odświeżania stanów rejestrów, to można to wykonywać na dwa sposoby:

  • Co określoną liczbę rozkazów
  • Co określony czas
    Raczej czas. Równomierne odstępy czasowe zagwarantują maksymalną jakość przy danym obciążeniu.
andrzejlisek napisał(a)

Co do obrazków (kontrolek PictureBox), to jedyna zastosowana metoda to podmiana obrazków na obrazek z pamięci poprzez "NazwaKontrolki.Image = ZmiennaZObrazkiem". Obrazki nie podlegają żadnym zmianom.
Podmiana ta musi być wykonana w wątku głównym.

A zamiast tego długaśnego switch'a (uh!) to sobie tablicę delegate'ów do funkcji, pogrupuj sobie te funkcję, powiąż z nimi informację ile razy trzeba wywołać DoOneCycle0 oraz DoOneCycle zamiast wstawiać wywołania w każdą funkcję.

0
gufiak napisał(a)

somekind, te typy istnieją dla wygody programisty, a nie szybkości działania. Żeby działało szybko, najlepiej używać tylko najprostszych typów i wykonywać na nich jak najmniej operacji.

adf88 napisał(a)

Trzymaj bity po 8 w jednym bajcie. Można sobie ułatić dostęp do konkretnych bitów takim rozszerzeniem

proponuje zerknac Reflectorem do BitVerctor32 lub BitArray, proponowanych juz przez somekind'a, i przestac pisac od nowa kolo - kod kod ku ktoremu zmierzacie zaczyna bardzo przypominac zawartosc tychze klas
chyba ze serio sadzicie ze w czystym C# napiszecie kod obslugujacy zestawy bitow szybszy niz jest w tych dwoch klasach? zreszta.. moze i sie da, nie probowalem.. jak potrzebujecie sensownej synchronizacji watkow wewnatrz tych obiektow, to nawet ma to spory sens
ale BitVector/Array na pewno i tak sa o wiele szybsze niz podejscie stosowane przez pierwotne autorskie rozwiazanie autora watku :/

0

Skorzystanie z BitVerctor32 czy BitArray nie będzie dobrym pomysłem. Z prostej przyczyny - prywatna wewnętrzna implementacja do której nie mamy dostępu. Dostępny jest jedynie ograniczony zestaw metod pozwalający robić to do czego te klasy są przeznaczone - traktować obiekt jako tablicę bitów. Nie dają możliwości odwołania się bezpośrednio do bufora bajtów, nie dają możliwości jednoczesnego operowania na grupach bitów.
Zauważ, że w symulatorze jedynie adresowanie jest bitowe, wszystkie operacje idą co najmniej w bajtach. Metody BitRegWrite, BitRegRead, PRead oraz pozostałe których autor nie przedstawił skomplikowały by się przy użyciu BitArray jeszcze bardziej niż pierwotny kod autora.

0

@adf88, właśnie o to mi chodziło. Dużo operacji odpada, kiedy przejdzie się z bool-a do byte. Ale stosowanie rozszerzeń do operacji na bitach, to znowu wygoda, nie wydajność. W C/C++ używało się preprocesora, dzięki czemu kod był zarazem czytelny, jak i wydajny. W C# nie mamy takiej możliwości, nie możemy nawet deklarować metod inline, co oznacza, że każde takie wywołanie funkcji to sporo więcej operacji niż bezpośrednie operowanie na bitach w kodzie. Przy wywołaniu funkcji muszą zostać przekazane parametry, oznacza to co najmniej kilka instrukcji więcej. Do tego dochodzi wywołanie funkcji, które czasami może trwać kilkanaście razy dłużej niż prosta operacja bitowa, np. AND.
Dla przykładu: metoda SetBit wygenerowała taki kod:

			sa = sa.SetBit(6);
00000100  mov         ecx,dword ptr [ebp-8] 
00000103  mov         edx,6 
00000108  call        dword ptr ds:[00AF0A70h] 
    00000000  push        ebp  
    00000001  mov         ebp,esp 
    00000003  sub         esp,8 
    00000006  mov         dword ptr [ebp-4],ecx 
    00000009  mov         dword ptr [ebp-8],edx 
    0000000c  cmp         dword ptr ds:[00139848h],0 
    00000013  je          0000001A 
    00000015  call        6B819C41 
    0000001a  movzx       eax,byte ptr [ebp-4] 
    0000001e  mov         ecx,dword ptr [ebp-8] 
    00000021  and         ecx,1Fh 
    00000024  mov         edx,1 
    00000029  shl         edx,cl 
    0000002b  or          eax,edx 
    0000002d  and         eax,0FFh 
    00000032  mov         esp,ebp 
    00000034  pop         ebp  
    00000035  ret
0000010e  mov         dword ptr [ebp-18h],eax 
00000111  movzx       eax,byte ptr [ebp-18h] 
00000115  mov         dword ptr [ebp-8]`code>Dla porównania, to samo "inline":`			ca |= 1 << 6;
0000013f  mov         eax,dword ptr [ebp-10h] 
00000142  or          eax,40h 
00000145  and         eax,0FFh 
0000014a  mov         dword ptr [ebp-10h],eax 

Dalszy komentarz chyba zbędny...

A zamiast tego długaśnego switch'a (uh!) to sobie tablicę delegate'ów do funkcji, pogrupuj sobie te funkcję, powiąż z nimi informację ile razy trzeba wywołać DoOneCycle0 oraz DoOneCycle zamiast wstawiać wywołania w każdą funkcję
O nie! Żadnych delegatów. Znowu, wygoda dla programisty - zabójstwo dla wydajności.
Długaśny switch to standard w symulatorach/emulatorach. Wszystkie inne kombinacje uderzają w szybkość symulacji. A delegaty to najwolniejsze cholerstwo jakie można tu wymyślić. Najlepsze byłyby metody inline, ale takich C# nie obsługuje.

@quetzalcoatl, nie dość, że obsługa BitArray czy tym bardziej BitVector32 jest trudniejsza niż operacje bitowe na typach liczbowych, to ich wydajność jest jeszcze gorsza od rozszerzeń. Przykład:

BitArray ba = new BitArray(8);
BitArray bb = new BitArray(new byte[] { 3 });
ba = bb.Not();
ba.Set(6, true);
ba.Xor(bb);

byte ca = 0;
byte cb = 3;
ca = (byte)~cb;
ca |= 1 << 6;
ca ^= cb;

Wygląda tak w assemblerze:

[CIACH!] // quetz, no bez jaj z ta iloscia stron

Sprawa jest prosta, albo przyspieszamy pisanie programu, albo przyspieszamy działanie programu. C# nie umożliwia połączenia jednego z drugim. .NET nie zostało stworzone, żeby programy były wydajne, lecz po to, żeby time-to-market był bardziej "wydajny".

0

gufiak:
nie musisz mi udowadniac co jest szybsze, bo wiem doskonale.. kodu assemblerowego tez nie masz co wklejac, poniewaz jak za pewne wiesz, na mojej maszynie IL moze byc przetworzony inaczej.. CLR nie optymalizuje takich rzeczy -- teraz nie, kiedys moze i bedzie. kompilator C++ jak sam powiedziales, widzac taki wynik pocialby to pewnie w cholere i zostalo by z tego pare linii..

jesli chodzi o wydajnosc a szybkosc pisania, to niestety, trzeba jeszcze uwzglednic jedna drobna rzecz: im 'krzakow' zamiast wywolan dobrze zdefiniowanych metod, tym latwiej o pomylke, ktorej potem bedziesz szukac nie wiadomo ile.
jesli ktos ma zamiast pisac maszyne wirtualna albo superszybki emulator, powinien zabraz sie za napisanie krytycznych rzeczy w c++ albo przynajmniej c++/cli zeby potem mu bylo latwo to polaczyc z gui w .net, albo jak cierpi na manię minimalizacji kodu maszynowego, tez pisac kluczowy kod w IL. jak sam powiedziales, nie po to stworzono .net zeby sie babrac, tylko dla wygody.. wiec albo-albo.

poza tym, powtarzam: konfrontowalem bitblah z oryginalnym kodem autora dzialajacym na worku bool'i z if/else..

poza tym #2:

adf88, masz racje:
BitVector udostepnia bufor, ale bez settera (to chamstwo..)
BitArray ma Item oraz Get, ale zwracajace bool, jedynie konstruktor pobiera int[]

tez tak sadzilem wczesniej, ale na post somekinda, przed napisaniem swojego sprawdzilem - i o zgrozo, zle przeczytalem zrzut metod z reflectora, zerknalem na nazwy metod/pol a nie spojrzalem na wartosci zwracane

niemniej, ja bym wzial bitarray (sealed, chamstwo #2), opakowal go swoja klasa i przez reflection wybebeszyl sobie referencje na jego m_array (ono jest realokowane tylko w ctor i Length/set, wiec latwo kontrolowac odswiezenie referencji), i podorabial reszte potrzebnych metod.. no ale ja i moje lenistwo to ja i nie wypada polecac :)

0

BitArray/BitVector32 ułatwiają operacje na pojedynczych bitach, a tu takowe nie są potrzebne.

gufiak napisał(a)

W C/C++ używało się preprocesora, dzięki czemu kod był zarazem czytelny, jak i wydajny. W C# nie mamy takiej możliwości, nie możemy nawet deklarować metod inline, co oznacza, że każde takie wywołanie funkcji to sporo więcej operacji niż bezpośrednie operowanie na bitach w kodzie. Przy wywołaniu funkcji muszą zostać przekazane parametry, oznacza to co najmniej kilka instrukcji więcej. Do tego dochodzi wywołanie funkcji, które czasami może trwać kilkanaście razy dłużej niż prosta operacja bitowa, np. AND
Weź też pod uwagę, że kompilator JIT tłumacząc kod pośredni na maszynowy optymalizuje go. Nie wiem na ile dobrze mu to wychodzi, ale to co jest wykonywane natywnie ma zupełnie inną postać. Testy szybkości kontenerów (sorry za brak linka, nie moge znaleźć) wykazują, że te .NET'owe są szybsze od SLT'owych. Dlaczego skoro nie ma inline'ów itp.? :> Otóż są ale dopiero w ostatnim etapie.

0

@adf88, weź pod uwagę, że zamieszczam przykłady w natywnym assemblerze, z włączonymi optymalizacjami ;)

0
gufiak napisał(a)

@adf88, weź pod uwagę, że zamieszczam przykłady w natywnym assemblerze, z włączonymi optymalizacjami ;)

pozwole sobie na rownie zasadny komentarz: wez pod uwage, ze to nie byl test w dobrze okreslonych warunkach eksperymentalnych, i zarowno na mojej maszynie i adf88 kod wynikowy moze wygladac inaczej, a i na Twojej w innych warunkach rowniez :)

adf88 napisał(a)

Testy szybkości kontenerów (sorry za brak linka, nie moge znaleźć) wykazują, że te .NET'owe są szybsze od SLT'owych. Dlaczego skoro nie ma inline'ów itp.? :> Otóż są ale dopiero w ostatnim etapie.

tez kojarze ten test.. ale z dyskusji o nim wynikalo, ze cos tam bylo nie tak i byl skrzywiony na korzysc .NET'owych.. nie pamietam niestety o co chodzilo.. jeslibys wykopal artykul przypadkiem, bylo by fajnie sobie przypomniec.
hm.. albo moze wieczorem po prostu puszcze pare tysiecy operacji i sprawdze z ciekawosci.

a tak wracajac do tematu - proponuje na razie skonczyc offtopa na temat CLR/JIT/BV/BA, bo chyba wszyscy sie juz zgadzaja w ten czy inny sposob, ze nalezy napisac wlasna klase, nie oparta na BV/BA, ktora opakuje potrzebne operacje bitowe

0

@quetzalcoatl,

nie musisz mi udowadniac co jest szybsze, bo wiem doskonale.. kodu assemblerowego tez nie masz co wklejac, poniewaz jak za pewne wiesz, na mojej maszynie IL moze byc przetworzony inaczej..

pozwole sobie na rownie zasadny komentarz: wez pod uwage, ze to nie byl test w dobrze okreslonych warunkach eksperymentalnych, i zarowno na mojej maszynie i adf88 kod wynikowy moze wygladac inaczej, a i na Twojej w innych warunkach rowniez
(O jakich warunkach piszesz?) Pytanie, jak inny możesz mieć u siebie kod? Ile procesorów z innym zestawem instrukcji niż x86, na których pójdzie .NET można znaleźć? Tu nie ma magii. Kod będzie się różnił na innym procesorze z rodziny x86, ale tylko w szczegółach. To zawsze będzie n (nawet nie 1.n) razy wolniej, gdy używasz oddzielnych funkcji, czy jakichkolwiek skomplikowanych typów dla prostych operacji. A używanie dla prostych operacji bitowych BitArray, czy BitVector32, to wynajdowanie kwadratowego koła na nowo... Nie do takich rzeczy te typy wymyślono. Umieć napisać program to nie to samo, co umieć napisać program dobrze. Równie dobrze można używać StringBuilder-a dla każdego stringa w programie...

CLR nie optymalizuje takich rzeczy -- teraz nie, kiedys moze i bedzie.
Kiedyś... A co mnie w dzisiaj pisanym programie interesuje co będzie za 5-10lat? Po pierwsze, nie masz gwarancji, że w ogóle tak będzie, po drugie, nie masz pojęcia, kiedy tak może być, po trzecie, klientowi nie wytłumaczysz, że program będzie za 5, może 10 lat działał szybciej... Zawsze możesz próbować, ale nie polecam...

jesli chodzi o wydajnosc a szybkosc pisania, to niestety, trzeba jeszcze uwzglednic jedna drobna rzecz: im 'krzakow' zamiast wywolan dobrze zdefiniowanych metod, tym latwiej o pomylke, ktorej potem bedziesz szukac nie wiadomo ile.
Oczywiście. Ale nie znaczy, że zawsze mamy iść na łatwiznę, bo boimy się, że coś schrzanimy. Coś musi mieć wyższy priorytet. Albo wydajność, albo łatwość pisania programu. Nieważne co zrobisz, to i tak trudniej pomylić się w zarządzanym C#, niż niezarządzanym C++. A jakoś nadal powstają świetne i niewykładające się programy w C++. Co ciekawe, zauważyłem, że najwięcej wykłada się programów pisanych w C#. Pisanie tylko pod .NET powoduje, że programiści stają się mniej uważni i bardziej leniwi. A nie można być dobrym programistą zawsze idąc na łatwiznę niezależnie od sytuacji. Całe szczęście, że kerneli nie piszą programiści C#...

jesli ktos ma zamiast pisac maszyne wirtualna albo superszybki emulator, powinien zabraz sie za napisanie krytycznych rzeczy w c++ albo przynajmniej c++/cli żeby potem mu bylo latwo to polaczyc z gui w .net
W C, ewentualnie C++ - ok. Gdybym ja tworzył ten program, to o .NET bym nawet nie pomyślał. Główny kod emulatora wrzuciłbym pewnie do biblioteki DLL pisanej w C, może nawet z fragmentami assemblera, a same GUI w C++ ew. C++/CLI.

albo jak cierpi na manię minimalizacji kodu maszynowego, tez pisac kluczowy kod w IL
Optymalizacja poprzez pisanie w IL... No bez jaj...

a tak wracajac do tematu - proponuje na razie skonczyc offtopa na temat CLR/JIT/BV/BA, bo chyba wszyscy sie juz zgadzaja w ten czy inny sposob, ze nalezy napisac wlasna klase, nie oparta na BV/BA, ktora opakuje potrzebne operacje bitowe
Zgadzam się, że nie należy używać BV/BA, ale nie zgadzam się, że do operacji na bitach trzeba tworzyć swoją klasę. Znowu wynajdywanie kwadratowego koła... Operacje bitowe to nie "rocket science", da się nimi operować łatwo i sprawnie bez próby opakowywania ich w ładne pudełko. Te pudełko bardziej uderzy w wydajność programu (przypominam - n razy wolniej) niż pomoże programiście w czytelności programu. Właśnie przez takie podejście później trzeba męczyć się z programami, które zamiast pracować, 90% czasu wyświetlają "Proszę czekać...".

0
gufiak napisał(a)

klientowi nie wytłumaczysz, że program będzie za 5, może 10 lat działał szybciej... Zawsze możesz próbować, ale nie polecam...

Pragnę przypomnieć, że w sytuacji o której mowa, nie ma klienta.

A jakoś nadal powstają świetne i niewykładające się programy w C++. Co ciekawe, zauważyłem, że najwięcej wykłada się programów pisanych w C#.

Jakieś statystyki, konkretne przykłady?
Bo mi najczęściej wywala się Firefox, on chyba nie jest w C# ;)

Całe szczęście, że kerneli nie piszą programiści C#...

To zdanie jest głupie. Pierdoła jest pierdołą bez względu na to, co robi, zaś generalizacje są dobre dla bawiących się w piaskownicy.
W swojej krótkiej karierze zauważyłem, że najgorszy kod produkują programiści C++ przesadzeni na C#. (Szczegóły zaraz wkleję do odpowiedniego wątku.)

Natomiast co do użycia czy prób użycia wbudowanych mechanizmów i różnych koncepcji wykonania danej funkcjonalności, to jest chyba jak najbardziej miejsce na to w tym przypadku. Wszakże to praca inżynierska i zawsze można zrobić jakieś porównanie wydajności, rozdział o tym napisać ;)

P.S. Nigdy nie używałem zaproponowanych przez siebie klas, po prostu wiem, że one istnieją i służą do mniej więcej takich rzeczy. Napisałem to tylko dla rozważenia.

0

Pragnę przypomnieć, że w sytuacji o której mowa, nie ma klienta.
Co nie zmienia faktu, że optymalizacja na zasadzie "bo to kiedyś może działać lepiej, jak MS bardziej się postara" jest, delikatnie mówiąc, zadziwiającym pomysłem.

Jakieś statystyki, konkretne przykłady?
Przepraszam, ale zgubiłem notatnik w którym zapisywałem sobie aplikacje, które mi się wyłożyły... Ale już na serio, używam sporo mniejszych lub większych programów i choć mało programów ogólnie się wykłada, to zauważam taką tendencję, że programy pisane pod .NET wykładają się nieco częściej.

Bo mi najczęściej wywala się Firefox, on chyba nie jest w C#
Ciekawe, bo całkiem dużo używam FF i jakoś już nie pamiętam kiedy ostatni raz mi się wyłożył. Sprawdź wtyczki, bo one często potrafią nieźle namieszać... Inna sprawa, że to spory program, często i długo używany, do tego jest to program opensource, więc jest duże prawdopodobieństwo wystąpienia błędów. Ja zaś mówię o programach mniej więcej wielkości, chyba znanego tutaj każdej osobie, programu Reflektor. Nie chodzi mi konkretnie o Reflektora, bo jest dobrze napisany, ale sporo programików tej wielkości lubi się często wykładać. W C++ oczywiście też, ale programów pisanych w C++ jest więcej, a mam wrażenie, że one rzadziej się wykładają.

To zdanie jest głupie. Pierdoła jest pierdołą bez względu na to, co robi, zaś generalizacje są dobre dla bawiących się w piaskownicy.
Nie, to zdanie nie jest głupie. Nie generalizuję. Nie stwierdzam, że programiści C# to kiepscy programiści, że są za głupi na pisanie kernela. Nie stwierdzam też, że wszyscy programiści piszący w językach zarządzanych mają złe nawyki. Ale .NET jednak wyrabia pewne, nie zawsze i nie w każdej sytuacji najlepsze nawyki. Pisząc pod .NET-a myślimy raczej jak zrobić coś ładniej, czytelniej, żeby jak najbardziej było odporne na błędy, żeby nie spędzić na pisaniu programu zbyt wiele czasu. W efekcie program pisze się szybko, bo wiele za nas robi framework i sporo mniej testowania trzeba, żeby wykryć błędy. Ale to prowadzi czasem do sytuacji, że mniej uważnie zajmujemy się obsługą błędów. W C/C++ jest nieco inaczej. Trzeba być bardziej uważnym, jest więcej sprawdzania, wszystkiemu trzeba przyjrzeć się przynajmniej dwa razy i tylko wtedy aplikacja ma szanse działać dobrze.
Także złym nawykiem, ale bezpośrednio wynikłym z działania frameworka, jest brak zastanawiania się przez wielu programistów nad optymalnością ich kodu. Parafrazując słowa wypowiedziane w tym temacie: "To że na twoim procesorze działa to tak, nie znaczy, że na moim nie będzie działać lepiej". Oczywiście, tylko na ile lepiej może to działać na innym procesorze, a na ile lepiej będzie działać jak my się bardziej postaramy? A równie dobrze na jeszcze innym procesorze kod natywny będzie jeszcze gorszy. Pisząc w C/C++ ma się bezpośrednie przełożenie na kod maszynowy i nieco bardziej zwraca się uwagę na optymalność kodu. Osobiście, jeśli mam napisać coś szybko, a nie musi być to hiper szybkie, piszę w C#, jeśli zaś zależy mi na szybkości działania, piszę w C/C++. Ale pisząc w C# nie zapominam, żeby zastanowić się, czy mój kod jest optymalny. Ale też zauważam jaki wpływ ma na mój styl i moją uwagę pisanie w C#. C# mnie rozleniwia, więcej błędów wybacza i o prawie każdym przewinieniu informuje. Czasem nie chce mi się nawet sprawdzać jakiegoś kodu, tylko uruchamiam i patrzę czy nie wyskoczy błąd. W C/C++ jest to dla mnie nie do pomyślenia. Tam dokładnie przeanalizuję kod, sprawdzę, czy przy obsłudze błędów rozpatrzyłem wszystkie potencjalne problemy.

Chodzi mi tylko o to, że podejście programisty C# w większości wypadków jest inne niż podejście programisty C++. Ten pierwszy skupi się na ułatwianiu sobie życia, ten drugi skupi się na ułatwianiu życia użytkownikowi programu. I tu też nie generalizuję, bo z tym różnie bywa, każdy programista jest inny, ale takie są ogólne tendencje.

W swojej krótkiej karierze zauważyłem, że najgorszy kod produkują programiści C++ przesadzeni na C#.
Też jak przesiadałem się na C# to tworzyłem różne dziwactwa. Choć oba języki są podobne do siebie, to przesiadka z jednego na drugiego nie trwa trzy dni. C++ sam jako język jest dosyć prosty, C# ma wiele różnych niuansów, jest ściśle powiązany z biblioteką, zupełnie nieznaną dla programistów C++, do tego jeszcze delegaty, eventy, właściwości, inne zasady dziedziczenia, nie znając tego nie da się napisać zbyt dobrego kodu w C#. Nawyki z C++ na początku przechodzą na C#, choćby z racji pozornego podobieństwa tych języków. Ich pozbycie się też trochę trwa. Ale tak na prawdę C++ wymaga zapamiętania mniejszej ilości zagadnień samego języka, lecz większej koncentracji i głębszego zrozumienia sprzętu na którym program ma działać. Dlatego też trochę to trwa, zanim człowiek przesiądzie się z C++ na C# i będzie pod C# pisał całkiem sprawnie.

Natomiast co do użycia czy prób użycia wbudowanych mechanizmów i różnych koncepcji wykonania danej funkcjonalności, to jest chyba jak najbardziej miejsce na to w tym przypadku.
Na to jest miejsce w przypadku programu, który dłuższy czas czeka aż użytkownik coś zrobi, a nie dla programu, który "żre" 100% procesora. A uzyskanie szybciej działającego kodu nie wymaga tu żadnych skomplikowanych czynności, lecz proste "x |= 1 << y", czy też "a &= ~0x40".

Ale muszę uderzyć się w pierś i przyznać, że po dokładnym sprawdzeniu nie we wszystkim miałem rację. Rozszerzenie zaproponowane przez adf88 pod pełną optymalizacją działa tak samo szybko jak operacje bitowe bezpośrednio w kodzie. A dzieje się tak dlatego, że owe cztery metody są bardzo krótkie i spełniają wszystkie warunki, żeby JIT skompilował je jako "inline". Więc gratuluję pomysłu i zwracam honor. Jeśli jednak chodzi o operacje na BitArray, to są przeważnie ok. 3 razy wolniejsze, zaś operacje na BitVector32, w zależności od użytej metody, są albo tak samo wydajne jak operacje bitowe we własnym kodzie (operacje na maskach - wtedy zachodzi kompilacja inline), albo 2-3 razy wolniejsze przy operowaniu na sekcjach.
Dopiero się doczytałem, że inlining metod w C# jest możliwy, ale jest w pełni kontrolowany przez JIT. Programista nie ma na to żadnego wpływu poza napisaniem metody spełniającej wszystkie warunki jakie bierze pod uwagę JIT przy decyzji czy kod wstawić inline, czy też w oddzielnym bloku. Inline'owane nie są:

  • metody, gdzie kod IL ma ponad 32 bajty,
  • wirtualne funkcje,
  • funkcje, w których przepływ sterowania jest bardziej skomplikowany (wszystko poza if/else),
  • metody zawierające bloki obsługi wyjątków,
  • metody w których którykolwiek z argumentów jest strukturą.
    Wszystko to również tyczy się właściwości. A więc jest trochę warunków do spełnienia. Ale fakt faktem, jest to możliwe.

Tak więc mea culpa, że czasem machnę coś bez dogłębnego sprawdzenia, ale mam nadzieję, że zawsze w takich przypadkach albo ja trafię na informacje, które zweryfikują moje poglądy/informacje, albo ktoś mi pokaże, że jest inaczej niż piszę. I po to są właśnie takie dyskusje, żeby przez wymianę poglądów/pomysłów/informacji, dowiedzieć się czegoś nowego i sprowokować siebie oraz innych do poszukiwania pewnych faktów, których normalnie by się nie szukało. Jeśli komuś wydaje się, że zawsze próbuję udowodnić, że mam rację, to się prawie nie myli ;) Najzwyczajniej w świecie staram się prowokować dyskusję. Gdybym dawno przyznał, macie wszyscy rację (słusznie czy też nie), dyskusji już by nie było.

Dyskusja o optymalności kodu jest ważna. Pamiętajcie, że zadaniem programistów nie jest pisanie ładnego kodu źródłowego, lecz pisanie dobrych programów. Program pisze się dla użytkownika, nie dla drugiego programisty, który po spojrzeniu w kod programu pochwali nas za świetną architekturę programu i styl. To są tylko narzędzia do uzyskania ostatecznego wyniku - dobrze działającego programu. To jaką drogą dotrze się do tego wyniku jest w pełni naszą indywidualną sprawą. A szybkość działania programu jest w niektórych wypadkach tak samo ważna, jak odporność programu na błędy. W pozostałych wypadkach jest nieomalże tak samo ważna.
Przykład z tego tematu jest modelowym przykładem programu, którego wydajność jest tak samo ważna jak odporność na błędy. Oczywiście, jest to inżynierka i nie musi to być super hiper szybki program, żeby była dobrze oceniona. Ale kto wie, czy w przyszłości autor programu, albo któryś z nas nie stworzy podobnego, komercyjnego programu np. na AVR albo może i nawet na ARM. A wtedy każda nanosekunda mniej to dodatkowa złotówka do kieszeni.

0

Mikrokontroler 8051 posiada pakiet bajtów adresowanych bitowo, to znaczy, że podaje się adres jednego bitu. W ten sposób można zaadresować 32 bajty.


public static class ByteBitExt
{
    public static bool GetBit(this Byte b, int n) { return  0 != (b &  (1 << n)); }
    public static byte SetBit(this Byte b, int n) { return (byte)(b |  (1 << n)); }
    public static byte ClrBit(this Byte b, int n) { return (byte)(b & ~(1 << n)); }
    public static byte TglBit(this Byte b, int n) { return (byte)(b ^  (1 << n)); }
}
//... przykładowo:
byte b = 2;
b = b.SetBit(5);


byte RegBits[] = new byte[32];


// Zapis rejestru z adresowaniem bitowym
public void BitRegWrite(uint Addr, byte Data)
{
    uint offset = Addr & 7; //szybsze "Addr % 8;"
    Addr >>= 3; //szybsze "Addr /= 8";
    uint mask = 0xFF << offset;
    RegBits[Addr] = (byte)(RegBits[Addr] & ~mask | (Data << offset));
    RegBits[Addr + 1] = (byte)(RegBits[Addr + 1] & mask | (Data >> (8 - offset)));
}

private byte BitRegRead(uint Addr)
{
    uint offset = Addr & 7; //szybsze "Addr % 8;"
    Addr >>= 3; //szybsze "Addr /= 8";
    return (byte)((RegBits[Addr] >> offset) | (RegBits[Addr + 1] << (8 - offset)));
}

byte[] Ports = new byte[4];
// Odczyt od wewnatrz z portu
private byte PRead(int Port)
{
    return Ports[Port] & RegBits[16 + 2 * Port];
}

Jezeli dobrze rozumiem, to zmienna RegBits ma być tablicą bajtów reprezentującą fragment pamięci z adresowaniem bitowym. W praktyce, już jest zmienna ConMem, która jest tablicą 256 bajtów

Tak wygląda ogólna metoda bajtowego odczytu i zapisu pamieci kontrolera:

        // Odczyt pamieci wewnetrznej
        private int IntRead(int Addr)
        {
            int Data = 0;
            if (Addr < 128)
            {
                if ((Addr < 32) || (Addr > 47))
                {
                    Data = ConMem[Addr];
                }
                else
                {
                    int AddrB = (Addr - 32) * 8;
                    bool B1 = RegBits[AddrB + 7];
                    bool B2 = RegBits[AddrB + 6];
                    bool B3 = RegBits[AddrB + 5];
                    bool B4 = RegBits[AddrB + 4];
                    bool B5 = RegBits[AddrB + 3];
                    bool B6 = RegBits[AddrB + 2];
                    bool B7 = RegBits[AddrB + 1];
                    bool B8 = RegBits[AddrB];
                    Data = BinToInt(B1, B2, B3, B4, B5, B6, B7, B8);
                }
            }
            else
            {
                if ((Addr % 8) == 0)
                {
                    Data = BitRegRead(Addr);
                    switch (Addr)
                    {
                        case 128:
                            Data = PRead(0);
                            break;
                        case 144:
                            Data = PRead(1);
                            break;
                        case 160:
                            Data = PRead(2);
                            break;
                        case 176:
                            Data = PRead(3);
                            break;
                    }
                }
                else
                {
                    switch (Addr)
                    {
                        case 129:
                            Data = SP;
                            break;
                        case 130:
                            Data = DPL;
                            break;
                        case 131:
                            Data = DPH;
                            break;
                        case 135:
                            Data = PCON;
                            break;
                        case 137:
                            Data = TMOD;
                            break;
                        case 138:
                            Data = TL0;
                            break;
                        case 139:
                            Data = TL1;
                            break;
                        case 140:
                            Data = TH0;
                            break;
                        case 141:
                            Data = TH1;
                            break;
                        case 153:
                            Data = BinToInt(SBUFFIN7, SBUFFIN6, SBUFFIN5, SBUFFIN4, SBUFFIN3, SBUFFIN2, SBUFFIN1, SBUFFIN0);
                            break;
                    }
                }
            }
            return Data;
        }

        // Zapis pamieci wewnetrznej
        private void IntWrite(int Addr, int Data)
        {
            //Debug.Text = Debug.Text + " " + Data.ToString();
            if (Addr < 128)
            {
                if ((Addr < 32) || (Addr > 47))
                {
                    ConMem[Addr] = Data;
                }
                else
                {
                    bool B1;
                    bool B2;
                    bool B3;
                    bool B4;
                    bool B5;
                    bool B6;
                    bool B7;
                    bool B8;
                    IntToBin(Data, out B1, out B2, out B3, out B4, out B5, out B6, out B7, out B8);
                    int AddrB = (Addr - 32) * 8;
                    RegBits[AddrB + 7] = B1;
                    RegBits[AddrB + 6] = B2;
                    RegBits[AddrB + 5] = B3;
                    RegBits[AddrB + 4] = B4;
                    RegBits[AddrB + 3] = B5;
                    RegBits[AddrB + 2] = B6;
                    RegBits[AddrB + 1] = B7;
                    RegBits[AddrB] = B8;
                }
            }
            else
            {
                if ((Addr % 8) == 0)
                {
                    BitRegWrite(Addr, Data);
                }
                else
                {
                    switch (Addr)
                    {
                        case 129:
                            SP = Data;
                            break;
                        case 130:
                            DPL = Data;
                            break;
                        case 131:
                            DPH = Data;
                            break;
                        case 135:
                            PCON = Data;
                            break;
                        case 137:
                            TMOD = Data;
                            break;
                        case 138:
                            TL0 = Data;
                            break;
                        case 139:
                            TL1 = Data;
                            break;
                        case 140:
                            TH0 = Data;
                            break;
                        case 141:
                            TH1 = Data;
                            break;
                        case 153:
                            IsTransmitting = true;
                            ComClockO = 0;
                            ComTransmitCounter = 0;
                            IntToBin(Data, out SBUFFOUT7, out SBUFFOUT6, out SBUFFOUT5, out SBUFFOUT4, out SBUFFOUT3, out SBUFFOUT2, out SBUFFOUT1, out SBUFFOUT0);
                            break;
                    }
                }
            }
        }

        // Odczyt rejestru z adresowaniem bitowym
        private int BitRegRead(int Addr)
        {
            int ret = RegBits[Addr + 7] ? 128 : 0;
            ret |= RegBits[Addr + 6] ? 64 : 0;
            ret |= RegBits[Addr + 5] ? 32 : 0;
            ret |= RegBits[Addr + 4] ? 16 : 0;
            ret |= RegBits[Addr + 3] ? 8 : 0;
            ret |= RegBits[Addr + 2] ? 4 : 0;
            ret |= RegBits[Addr + 1] ? 2 : 0;
            ret |= RegBits[Addr] ? 1 : 0;
            return ret;
            /*
            int B7 = Convert.ToInt16(RegBits[Addr + 7]) * 128;
            int B6 = Convert.ToInt16(RegBits[Addr + 6]) * 64;
            int B5 = Convert.ToInt16(RegBits[Addr + 5]) * 32;
            int B4 = Convert.ToInt16(RegBits[Addr + 4]) * 16;
            int B3 = Convert.ToInt16(RegBits[Addr + 3]) * 8;
            int B2 = Convert.ToInt16(RegBits[Addr + 2]) * 4;
            int B1 = Convert.ToInt16(RegBits[Addr + 1]) * 2;
            int B0 = Convert.ToInt16(RegBits[Addr]);
            return B7 + B6 + B5 + B4 + B3 + B2 + B1 + B0;
            */
        }

        // Zapis rejestru z adresowaniem bitowym
        private void BitRegWrite(int Addr, int Data)
        {
            bool B7;
            bool B6;
            bool B5;
            bool B4;
            bool B3;
            bool B2;
            bool B1;
            bool B0;
            IntToBin(Data, out B7, out B6, out B5, out B4, out B3, out B2, out B1, out B0);
            RegBits[Addr + 7] = B7;
            RegBits[Addr + 6] = B6;
            RegBits[Addr + 5] = B5;
            RegBits[Addr + 4] = B4;
            RegBits[Addr + 3] = B3;
            RegBits[Addr + 2] = B2;
            RegBits[Addr + 1] = B1;
            RegBits[Addr] = B0;
        }

Ponieważ pierwsze 128 bajtów jest pamięcią RAM, w programie jest:

        int[] ConMem = new int[128];

W takim razie myślę, że można zrezygnować z RegBits i zrobić cos takiego:

        int[] ConMem = new int[256];

        // Odczyt pamieci wewnetrznej
        private int IntRead(int Addr)
        {
            int Data = 0;
            if (Addr < 128)
            {
                Data = ConMem[Addr];
            }
            else
            {
                if ((Addr % 8) == 0)
                {
                    Data = ConMem[Addr];
                    switch (Addr)
                    {
                        case 128:
                            Data = PRead(0);
                            break;
                        case 144:
                            Data = PRead(1);
                            break;
                        case 160:
                            Data = PRead(2);
                            break;
                        case 176:
                            Data = PRead(3);
                            break;
                    }
                }
                else
                {
                    switch (Addr)
                    {
                        case 129:
                            Data = SP;
                            break;
                        case 130:
                            Data = DPL;
                            break;
                        case 131:
                            Data = DPH;
                            break;
                        case 135:
                            Data = PCON;
                            break;
                        case 137:
                            Data = TMOD;
                            break;
                        case 138:
                            Data = TL0;
                            break;
                        case 139:
                            Data = TL1;
                            break;
                        case 140:
                            Data = TH0;
                            break;
                        case 141:
                            Data = TH1;
                            break;
                        case 153:
                            Data = BinToInt(SBUFFIN7, SBUFFIN6, SBUFFIN5, SBUFFIN4, SBUFFIN3, SBUFFIN2, SBUFFIN1, SBUFFIN0);
                            break;
                    }
                }
            }
            return Data;
        }

        // Zapis pamieci wewnetrznej
        private void IntWrite(int Addr, int Data)
        {
            //Debug.Text = Debug.Text + " " + Data.ToString();
            if (Addr < 128)
            {
                ConMem[Addr] = Data;
            }
            else
            {
                if ((Addr % 8) == 0)
                {
                    ConMem[Addr] = Data;
                }
                else
                {
                    switch (Addr)
                    {
                        case 129:
                            SP = Data;
                            break;
                        case 130:
                            DPL = Data;
                            break;
                        case 131:
                            DPH = Data;
                            break;
                        case 135:
                            PCON = Data;
                            break;
                        case 137:
                            TMOD = Data;
                            break;
                        case 138:
                            TL0 = Data;
                            break;
                        case 139:
                            TL1 = Data;
                            break;
                        case 140:
                            TH0 = Data;
                            break;
                        case 141:
                            TH1 = Data;
                            break;
                        case 153:
                            IsTransmitting = true;
                            ComClockO = 0;
                            ComTransmitCounter = 0;
                            IntToBin(Data, out SBUFFOUT7, out SBUFFOUT6, out SBUFFOUT5, out SBUFFOUT4, out SBUFFOUT3, out SBUFFOUT2, out SBUFFOUT1, out SBUFFOUT0);
                            break;
                    }
                }
            }
        }


Co do odczytu bitów w ramach wykonywania rozkazów, uwzględnia się port P3, dlatego jest:

        // Odczyt bitu z adresu
        private bool BitRead(int Addr)
        {
            bool Res = RegBits[Addr];
            if ((Addr > 175) && (Addr < 184))
            {
                Res = RegBits[Addr] && P3[Addr - 176];
            }
            return Res;
        }

W przypadku bezośredniego odwołania sie do bitów:
Dla adresów bitowych od 0 do 127 mapuje się bajty pamięci RAM o adresach od 32 do 47.
Dla adresów od 128 do 255 mapuje się na adresy rejestrów w ten sposób, że od podanego adresu odejmuje się resztę z dzielenia przez 8 (ta reszta to numer bitu) i otrzymuje się adres rejestru.
Przykład:
Dla adresów od 80h do 87h: Rejestr 80h
Dla adresów od 88h do 8Fh: Rejestr 88h
Dla adresów od 90h do 97h: Rejestr 90h
Dla adresów od 98h do 9Fh: Rejestr 98h
I tak dalej.
Przykłady obliczania adresów i bitów:
Adres 97: Rejestr 90, bit 7
Adres A3: Rejestr A0, bit 3
Adres CA: Rejestr C8, bit 2

Zamiast:

A = RegBits[adr1];
RegBits[adr2] = B;

byłoby:

A = MemBitRead(adr1);
MemBitWrite(adr2, B);

int[] ConMem = new int[256]; // jako pamięć wykorzysta się 128 elementów, a reszta (ale tylko niektóre elementy) do rejestrów z adresacją bitową.

public static class ByteBitExt
{
    public static bool GetBit(this Byte b, int n) { return  0 != (b &  (1 << n)); }
    public static byte SetBit(this Byte b, int n) { return (byte)(b |  (1 << n)); }
    public static byte ClrBit(this Byte b, int n) { return (byte)(b & ~(1 << n)); }
    public static byte TglBit(this Byte b, int n) { return (byte)(b ^  (1 << n)); }
}

private bool MemBitRead(int Addr)
{
    if (Addr < 48)
    {
        ConMem[(Addr >> 3) + 32].GetBit(Addr & 7); //przesuniecie zamiast (Addr / 8) + 32 i Addr % 8
    }
    else
    {
	ConMem[Addr & 248].GetBit(Addr & 7);
    }
}

private bool MemBitWrite(int Addr)
{
    if (Addr < 48)
    {
        int A = (Addr << 3) + 32; 
        ConMem[A] = ConMem[A].SetBit(Addr & 7); //przesuniecie zamiast (Addr / 8) + 32 i Addr % 8
    }
    else
    {
        int A = Addr & 248; 
	ConMem[A] = ConMem[A].SetBit(Addr & 7);
    }
}

Odczyt z uwzględnieniem portu po zmianach:

        // Odczyt bitu z adresu
        private bool BitRead(int Addr)
        {
            bool Res = MemBitRead[Addr];
            if ((Addr > 175) && (Addr < 184))
            {
                Res = MemBitRead[Addr] && P3[Addr - 176];
            }
            return Res;
        }

Czy tak będzie dobrze?

Co do wielowątkowości, to uruchomiłem pętlę w drugim wątku, i jeśli chodzi o odświeżania w regularnych odstepach czasu, to udało mi się uzyskać zamierzony efekt, tą sytuacje przedstawia lewy rysunek.
Jesli chodzi o odświeżanie co określona ilość rozkazów (w przypadku śledzenia szybko zachodzacych zmian, kosztem szybkosci programu), to początkowo zrobiłem według środkowego rysunku, ale dochodziło do błędów w odświeżaniu, to znaczy, że nie odświezały się wszystkie elementy wyświetlacza LCD (32 bitmapy 10x16 pikseli). Po zrealizowaniu sposobu, jak na prawym rysunku, problem pozostał. Rozwiazalo go dodanie:

PanelLcd.Refresh();

PanelLcd to panel, na którym są połoone te bitmapy. Jednak to odświeżanie spowalnia trochę.
Jak jest odświeżanie z timera, to ten problem nie istnieje (działa według lewego rysunku).

Na rysunku:
Zółty: Wątek GUI
Zielony: Wyświetlanie stanu
Czerwony: Wykonywanie iteracji pętli (jeden rozkaz 8051)
user image

0
gufiak napisał(a)

Co nie zmienia faktu, że optymalizacja na zasadzie "bo to kiedyś może działać lepiej, jak MS bardziej się postara" jest, delikatnie mówiąc, zadziwiającym pomysłem.

Jasne.
Ale nie chcę pokazywać palcem, kto sprowadził problem wykonania pracy inżynierskiej do kwestii wydajnościowych platformy .NET ;)

Ale już na serio, używam sporo mniejszych lub większych programów i choć mało programów ogólnie się wykłada, to zauważam taką tendencję, że programy pisane pod .NET wykładają się nieco częściej.

Screenshot or it never happened.
Nie to, że Ci nie wierzę, bo różne aplikacje się wykładają, po prostu takie pisanie jest bez sensu. Jak każda generalizacja.
Jeśli nie piję alkoholu i nie kradnę samochodów, to nie jestem Polakiem? Bo przecież wszyscy Polacy to robią.

Ciekawe, bo całkiem dużo używam FF i jakoś już nie pamiętam kiedy ostatni raz mi się wyłożył.

Średnio raz na miesiąc pada. Co prawda potem się elegancko przywraca i nie ma problemu, ale nie widzę związku między stabilnością a technologią wykonania. (Screenshota zrobię następnym razem. Chociaż pewno dopadnie mnie pech i teraz się już nigdy nie wywali, zgodnie z prawami Murphy'ego.)

Nie stwierdzam, że programiści C# to kiepscy programiści, że są za głupi na pisanie kernela.

Całe szczęście, że kerneli nie piszą programiści C#...

Więc o jakie szczęście chodzi?

Ale .NET jednak wyrabia pewne, nie zawsze i nie w każdej sytuacji najlepsze nawyki. Pisząc pod .NET-a myślimy raczej jak zrobić coś ładniej, czytelniej, żeby jak najbardziej było odporne na błędy, żeby nie spędzić na pisaniu programu zbyt wiele czasu.

Nawyki, które .NET wyrabia w Tobie nie są nawykami, które .NET wyrabia we wszystkich. Ciągle generalizujesz (na dodatek twierdząc, że tego nie robisz...).

Także złym nawykiem, ale bezpośrednio wynikłym z działania frameworka, jest brak zastanawiania się przez wielu programistów nad optymalnością ich kodu.

Nie wiem jak inni, ja tam się zastanawiam ;P
Dziś np. cały dzień poprawiałem kod pewnej kontrolki, która prawie po każdym kliknięciu w interfejs grzebała w bazie. Nagle zaczęła działać 10 razy szybciej. Ciekawe o czym myślał ekspert, który to pisał.

Pisząc w C/C++ ma się bezpośrednie przełożenie na kod maszynowy i nieco bardziej zwraca się uwagę na optymalność kodu.

Niedawno miałem małą fuchę - program do prostego przetwarzania grafiki, pierwotnie napisany w C++. "Przepisałem" do C# i np. poszarzanie zdjęcia trwa 120ms zamiast 20 sekund.
Nie zwraca się, nie generalizuj. Część programistów zwraca, część nie, a część myśli, że optymalizacja to jakaś choroba weneryczna.
Na każde n przykładów rewelacyjności jakiejś technologii znajdzie się m kontrprzykładów świadczących o jej beznadziejności. Na każde x dobrych programistów jednego języka znajdzie się y złych.

Czasem nie chce mi się nawet sprawdzać jakiegoś kodu, tylko uruchamiam i patrzę czy nie wyskoczy błąd. W C/C++ jest to dla mnie nie do pomyślenia. Tam dokładnie przeanalizuję kod, sprawdzę, czy przy obsłudze błędów rozpatrzyłem wszystkie potencjalne problemy.

To raczej kwestia tego, że kompilacja w C++ trwa 20 razy dłużej ;)

Chodzi mi tylko o to, że podejście programisty C# w większości wypadków jest inne niż podejście programisty C++. Ten pierwszy skupi się na ułatwianiu sobie życia, ten drugi skupi się na ułatwianiu życia użytkownikowi programu. I tu też nie generalizuję, bo z tym różnie bywa, każdy programista jest inny, ale takie są ogólne tendencje.

Generalizujesz i pieprzysz od rzeczy na dodatek. Dobry programista skupi się na obu tych aspektach, bo nie lubi tracić swojego czasu i tworzy programy dla użytkowników. Znowu bez względu na język.

C++ sam jako język jest dosyć prosty,

Chodzi miliard sposobów na rzutowanie, używanie wskaźników do praktycznie wszystkiego czy może jego kontekstowość? ;>

Na to jest miejsce w przypadku programu, który dłuższy czas czeka aż użytkownik coś zrobi, a nie dla programu, który "żre" 100% procesora. A uzyskanie szybciej działającego kodu nie wymaga tu żadnych skomplikowanych czynności, lecz proste "x |= 1 << y", czy też "a &= ~0x40".

Nie wiem, dlaczego wyciągnąłeś moje zdanie z kontekstu i nie zrozumiałeś przesłania mojej wypowiedzi, które było jasne. Porównanie wydajności gotowych i własnych rozwiązań posiada merytoryczną wartość naukową i na to na pewno jest miejsce w ramach pracy prawie-że-naukowej, jaką jest praca inżynierska. To była tylko taka moja mała rada dla autora.

Ale kto wie, czy w przyszłości autor programu, albo któryś z nas nie stworzy podobnego, komercyjnego programu np. na AVR albo może i nawet na ARM. A wtedy każda nanosekunda mniej to dodatkowa złotówka do kieszeni.

Ale to takie samo gdybanie jak z tą wydajnością kompilacji za n lat, gdy M$ się bardziej postara.

0

powinienem pociac ten watek z offtopikow, ktore obecnie zajmuja wiecej niz tresc wlasciwa, ale tego wyjatkowo nie zrobie, poniewaz sadze ze wypowiedzi sa wystarczajaco ciekawe i/lub w napisanie ich autorzy wlozyli czas, wiec nalezy uszanowac..

gufiak: wiem, ze dokopales sie juz do optymalizacji w przypadku BV i to co pisze juz jest spoznione, ale te 'riposte' musze napisac, poniewaz Twoj post odebralem wybitnie osobiscie, jak i rownie personalnie odbieram pozniejsze wzmianki nt. "mało myslacych programistow C#" itp. mylisz sie sromotnie, jesli mnie za takiego uwazasz. [btw. podobnie mylisz sie stwierdzajac ze programista C/C++ musi wiedziec mniej o bibliotekach niz C#.. przypomnij sobie boost'a lambda, spirit czy mpl na przyklad i np. dorzuc do tego proby uzywania ich w srodowisku wielowatkowym] rozumiem Twoje zacietrzewienie w temacie, tez czasem mi sie to zdarza, ale Twoja wylewna odpowiedz na moj ostatni post jest juz kompletnie pozbawiona sensu i miejsca, chyba ze ma to byc jakas walka o pozycje w stadzie, czegoby tym bardziej juz nie rozumial.. przyczepianie sie do moich sformulowan "o stworzeniu metody" - jest spudlowane - poniewaz mialem na mysli metody ZBIORCZE, opakowujace komplet operacji bitowych do wykonania w danym kroku, nabudowane na BitVectorze czy BitArray a nie uzywajace ich na chama w oryginalnej formie, nie sadze zebys mial zamiar udowadniac wyzszosc robienia i trzymania wszystkiego wszedzie na biezaco olewajac podstawowe zasady modularyzacji/enkapsulacji. odnosnie generowania kodu maszynowego z IL: specjalnie odnioslem sie do slynnych 'warunkow eksperymentalnych', zeby zwrocic Ci drobna uwage miedzy wierszami, ale zmuszasz mnie do mowienia wprost: moge sie mylic, ale sie zaloze, ze nie wiesz, jak i kiedy CLR zdecyduje sie cos rozwinac albo zoptymalizowac na roznych typach architektur czy wrecz modelach prockow, wiec nie masz formalnego prawa twierdzic, ze cos bedzie zawsze generowalo taki a nie inny kod maszynowy zachowujacy sie lepiej albo gorzej. formalne takie prawo miec bedziesz, jesli uda Ci sie znalezc dowod w postaci dokumentacji stwierdzajacej tak a nie inaczej, kodu platformy swiadczacego o tym, lub zbioru statystyk zebranych empirycznie z 'istotnego statystycznie fragmentu populacji' oraz odpowiednio zroznicowanego. nawiazuje do tego nie z celem pomieszania Cie z blotem, uciszenia Cie, czy udowodnienia swojej wyzszosci przez 'pseudonaukowe' gadanie, ale by zwrocic Ci uwage, ze w swoich wypowiedziach wypada stosowac 'nacisk' adekwatny nie do tego co nam sie wydaje czy co wiemy, ale do tego, co mozemy udowodnic w sposob nie dajacy sie podwazyc, a w przypadkach gdy tego sie zrobic nie da z jakiegos powodu, wypada zachowac troche rezerwy/pokory/skromnosci na wypadek pomylki. I szczerze, nie widac jej u Ciebie nawet w w akapicie zaczynajacym sie od 'mea culpa'
ps. jesli prawie zawsze probujesz udowodnic ze masz racje, to pewnie czekaja nas bardzo dlugie dyskusje:) o ile moja zbyt mala pula czasu poswiecana na 2+ dzialy na forum bedzie na to pozwalala.. bo ja tez tak mam, ale % czasu wolnego nie pozwala na folgowaniu sobie i kopaniu w kazdym temacie w ktorym wydaje mi sie ze ktos cos zle lub nie-do-konca-w-pelni powiedzial..

apeluje ponownie o powrot do tematu oryginalnego - somekind, do Ciebie tez, adf88 - na wszelki wypadek rowniez prosze o to.
poczawszy od 5tego postu nastepujacego po tym, zeby mnie z kolei nikt nie posadzal o nadawanie sobie prawa ostatniego slowa (ktore teoretycznie mam :P), bede cial wszystkie wyrazne zejscia z tematu, czyli kto-komu-co-wytknie-bo-powiedzial-ze-moze-MS-cos-kiedys etc

0

Zacznę od prośby, żeby nie brać tutaj niczego osobiście, a czasem nawet z przymrużeniem oka, bo to nie jest konferencja naukowa. Jeśli komuś wygodniej tak myśleć, to niech uważa, że generalizuję. Nie mam nikomu za złe (poza sobą), że mógł to tak potraktować, gdyż rzuciłem pewien skrót myślowy bez rozwinięcia go od razu. Ale dla jasności jeszcze raz piszę: opisuję tylko tendencje jakie dostrzegam. Nie dane statystyczne, gdyż nie jest to publikacja naukowa, lecz mój wpis na forum. Są to w pełni subiektywne spostrzeżenia wyrobione po kilku latach obserwacji. Nie musicie się ze mną zgadzać. Uważam jedynie, że takie tendencje istnieją głównie z dwóch powodów: po pierwsze, dzięki .NET programować zaczynają ludzie, którzy bez .NET-a by sobie nie poradzili, po drugie, za bardzo naciska się dzisiaj na sztywne trzymanie się jednego paradygmatu. Jak już wcześniej pisałem - to jest droga do celu, nie cel sam w sobie.. I nie piszę tu tylko o domorosłych programistach, ale również o ludziach po studiach informatycznych. Dla przykładu: http://www.codinghorror.com/blog/archives/000781.html
A prosiłem o nieodbieranie tego co piszę osobiście, gdyż widzę, że Wasza wiedza w temacie jest nie byle jaka i myślę, że jesteście zdolnymi programistami. No ale nie jesteście jedynymi programistami. Jest pełno programistów C/C++, którzy nie mają zielonego pojęcia o optymalizacji, tak samo jest bez liku programistów C#, którzy mają większe pojęcie o optymalizacji niż większość programistów C/C++, ale pytanie jak rozkładają się proporcje. Moim zdaniem, ze względu na większą abstrakcję języka C#, a także przyczynę powstania platformy .NET, mniej programistów tego języka przejmuje się optymalnością kodu. Bierze się to również z podejścia różnych firm, gdzie ilość jest przedkładana nad jakość. W ten sposób programiści takich firm wyrabiają sobie pewne przyzwyczajenia, przez co pojęcie optymalności kodu staje im się nieznane.

@Quetz, chciałbym zauważyć, że jak na razie tylko ja zadałem sobie trud, żeby spojrzeć w kod generowany przez JIT. Ale masz rację z tym, że nie wiem jakie CLR podejmuje decyzje. A przynajmniej nie wiem do końca. Ale na podstawie natywnego kodu jestem w stanie określić czy da się zrobić coś lepiej, czy też nie. Wiem też, jak mniej więcej podobny kod będzie wyglądał i wykonywał się na innych procesorach. Jestem elektronikiem, oprogramowywałem już trochę procesorów. Znam architektury najpopularniejszych procesorów i wiem co jest możliwe, a co niekoniecznie. Gdybym pisał jakąś publikację naukową, pewnie bym tak zrobił jak piszesz. Ale w przypadku wpisu na forum uważam, że wystarczające jest, gdy jestem w stanie z dużym prawdopodobieństwem stwierdzić, czy JIT jest w stanie poradzić sobie lepiej na innej architekturze. Niestety, .NET na razie wspiera zaledwie 4 platformy, z czego tylko jedna bardziej różni się od pozostałych - ARM. Ale na ARMie szybciej to nie pójdzie.

Tak samo zakładając, że na innym procesorze JIT może wygenerować lepszy kod, należałoby założyć, że może wygenerować również gorszy kod. Nie każdy ma w domu, czy nawet w pracy, superkomputer z jakimś kosmicznym procesorem. My przeważnie nie mamy wpływu na to na jakim procesorze będzie uruchamiany program, a na pewno nie mamy wpływu na działanie JIT. Jedynie mamy wpływ na kod, który piszemy. Jeśli napiszemy optymalniejszy kod, to będzie on w 99% przypadków optymalniejszy na każdym procesorze. Jak na razie JIT generuje kod na podstawie tego co piszemy. Może kiedyś, w przyszłości, będzie on dokładnie analizował wypociny programisty i będzie optymalizował algorytmy, a nie tylko kod, ale to nie ma miejsca dzisiaj i dzisiaj optymalność kodu to również działka programisty.

Jedyne co mogłoby przyspieszyć ów kod wykorzystujący BA/BV(sekcje), czy własne, nieco bardziej rozbudowane typy/metody, to mniej restrykcyjne zasady inliningu. Ale jest jasno określone jakie funkcje są inline-owane, co napisałem w swoim poprzednim poście, i choć opisane zasady mogą ulec zmianie, to przy optymalizacji nie można tego rozważać, bo to znów gdybanie. A praktycznie na każdym procesorze wywołanie procedury (call), jest jedną z najdłużej wykonujących się instrukcji. Do tego, jeśli do wywoływanej funkcji przekazywane są parametry, to kompilatory zawsze najpierw umieszczają je w odpowiednich rejestrach (i/lub umieszczają na stosie, co również w wielu procesorach jest jedną z wolniejszych instrukcji), by potem w ciele funkcji przemieścić je do rejestrów roboczych, na których będą wykonywane operacje. I to jest cecha kompilatora, nie procesora. Na dzień dzisiejszy każdy znany mi kompilator tak robi. Jedyny sposób na uniknięcie sporego zestawu dodatkowych instrukcji związanych z wywołaniem podprocedury jest inlining... A to co różni procesory z rodziny x86 (w tym i 64-bitowe wersje) pod względem instrukcji, to rozszerzenia jak MMX, SSEn itp. Ale te rozszerzenia wiele nie zmieniają przy wywoływaniu podprocedur, a jeśli już przyspieszą działanie wbudowanych funkcji, to również przyspieszą działanie naszego kodu. Więc proporcje będą nadał podobne. Stąd moja uwaga, że gdy optymalność kodu jest najważniejsza, należy inaczej podejść do modularyzacji kodu. Programista jest w stanie poradzić sobie z mniej modularnym kodem, procesor nie poradzi sobie szybciej, a przeważnie nawet tak samo, z kodem bardziej modularnym... I to jest kwestia wyboru priorytetów.

I prośba do wszystkich. Proszę o zachowanie emocji na niskim poziomie. Proszę o nie wyjeżdżanie z pozycjami w stadach, przyprawami itp. To nie o to tutaj chodzi. Chodzi o rzeczową dyskusję, wymianę poglądów. Wymiana złośliwości, poddawanie się emocjom, sympatie, antypatie, to nie metody dyskusji na forum programistycznym. W temacie poruszane są drażliwe tematy i dlatego tym bardziej wypadałoby podchodzić do tego z dystansem. Jeśli nie potraficie, to kończymy dyskusję. Bo zgadzać się ze mną, jak najbardziej, nie musicie. Ale pewne granice wypadałoby zachować.

I mam jeszcze prośbę do moderatorów. Jeśli macie możliwość, to jednak wydzielcie offtop do oddzielnego wątku, bo faktycznie wyszło dużo filozofii, a nikt nie chce pomagać koledze w programie. Ja swoją odpowiedź do tematu napiszę w oddzielnym poście na wszelki wypadek, jeśli offtop miałby trafić do oddzielnego wątku.

// spoko, spoko - Off-topic'ki wyżej ciężko usunąć bo meritum się plącze z "emocjami". Jednak dalsza dyskusja będzie monitorowana (bądź co bądź - dość ciekawa i na wysokim poziomie, co ostatnio nie jest częste) - Deti

0

@andrzejlisek, to że 8051 jest adresowany bitowo, nie znaczy, że w programie musisz każdy bit trzymać oddzielnie. W większości wypadków program będzie czytał i zapisywał pełne bajty i wypadałoby pod tym kątem optymalizować. RegBits możesz śmiało zrobić jako tablicę elementów typu byte, albo właśnie z nich zrezygnować. W większości wypadków przyspieszy to program.

Druga uwaga: używaj "default" w blokach switch, zamiast dwukrotnie wczytywać zmienną "Data".

I dziwi mnie, że uparcie używasz wszędzie liczb dziesiętnych. W większości sytuacji liczby szesnastkowe byłyby bardziej wymowne.

A co do panela, to spróbuj użyć double-bufferingu. Myślę, że w tym przypadku powinno pomóc.

0
gufiak napisał(a)

I prośba do wszystkich. Proszę o zachowanie emocji na niskim poziomie. Proszę o nie wyjeżdżanie z pozycjami w stadach, przyprawami itp. To nie o to tutaj chodzi. Chodzi o rzeczową dyskusję, wymianę poglądów. Wymiana złośliwości(..)

Masz racje - to bylo z mojej strony bezzasadne, pomylilem jeden z postow somekind'a z Toba/Twoim, i myslalem ze to Ty wlasnie wchodzisz w taki ton jaki w efekcie mi wyszedl

@wydzielenia watku -- niestety, nasze forum jeszcze nie dorobilo sie funkcjonalnosci "dzielenie watkow", a raz ze reczne kopiowanie/fingowanie watku i postow to irytujaca robota, a dwa - patrz przypis Deti'ego

1 użytkowników online, w tym zalogowanych: 0, gości: 1