Stack na lockach szybszy od lock-free?

Stack na lockach szybszy od lock-free?
BM
  • Rejestracja:około 5 lat
  • Ostatnio:około 5 lat
  • Postów:3
0

Mam dwie implementacje stosu, jedną opartach na lockach, a drugą non-blocking:

Kopiuj
package ds;

import java.util.concurrent.atomic.AtomicInteger;

public class BlockingStack<E> {

    private Node<E> head = new Node<>();

    private final AtomicInteger size = new AtomicInteger();

    public synchronized void push(E e) {
        final Node<E> newNode = new Node<>();
        newNode.value = e;
        newNode.next = head;
        head = newNode;
        size.getAndIncrement();
    }

    public synchronized E pop() {
        final Node<E> oldHead = head;
        if (oldHead == null) {
            return null;
        }
        head = oldHead.next;
        size.getAndDecrement();
        return oldHead.value;
    }

    public int size() {
        return size.get();
    }

    class Node<E> {
        E value;
        Node<E> next;
    }
}

Kopiuj
package ds;

import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.locks.LockSupport;

public class NonBlockingStack<E> {

    private final AtomicReference<Node<E>> head =
            new AtomicReference<>();

    private final AtomicInteger size = new AtomicInteger();

    public int size() {
        return size.get();
    }

    public void push(E e) {
        Node<E> newNode = new Node<>();
        newNode.value = e;
        Node<E> oldHead;
        do {
            oldHead = head.get();
            newNode.next = oldHead;
        } while (!compareAndSet(oldHead, newNode));
        size.getAndIncrement();
    }

    public E pop() {
        Node<E> oldHead;
        Node<E> newHead;

        do {
            oldHead = head.get();
            if (oldHead == null) {
                return null;
            }
            newHead = oldHead.next;
        } while (!compareAndSet(oldHead, newHead));
        size.getAndDecrement();
        return oldHead.value;
    }


    public boolean compareAndSet(final Node<E> current, final Node<E>  next) {
        if (head.compareAndSet(current, next)) {
            return true;
        } else {
            LockSupport.parkNanos(1);
            return false;
        }
    }

    class Node<E> {
        E value;
        Node<E> next;
    }
}


Chciałem przetestować wydajność obu implementacji przy różnych ilościach wątków/operacji:

Kopiuj
package buffers;

import ds.BlockingStack;
import ds.NonBlockingStack;

import java.util.concurrent.CountDownLatch;

public class Main {

    private static final int NUM_OF_THREADS = 8;
    private static final int OPERATIONS = 100_000;
    private static final Thread[] THREADS = new Thread[NUM_OF_THREADS];

    public static void main(String[] args) throws InterruptedException {
        final BlockingStack<Integer> ints = new BlockingStack<>();
//        final NonBlockingStack<Integer> ints = new NonBlockingStack<>();

        final CountDownLatch cdl = new CountDownLatch(NUM_OF_THREADS + 1);

        for (int i = 0; i < NUM_OF_THREADS; i++) {
            THREADS[i] = new Thread(() -> {
                try {
                    cdl.countDown();
                    cdl.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < OPERATIONS; j++) {
                    ints.push(j);
                }
            });
        }

        for (Thread t : THREADS) {
            t.start();
        }

        long start = System.nanoTime();
        cdl.countDown();
        for (Thread t : THREADS) {
            t.join();
        }
        long elapsed = System.nanoTime() - start;

        System.out.println("elapsed: " + elapsed);
        System.out.println("size: " + ints.size());

        long nsPerItem = elapsed / (NUM_OF_THREADS * (long) OPERATIONS);
        System.out.print("throughput: " + nsPerItem + " ns/item");
    }

}

Efekt jest taki, że przykładowo dla 8 wątków, z 100.000 operacjami stack z lockami jest szybszy/taki sam niż/jak ten nieblokujący. Czemu?

edytowany 6x, ostatnio: BialyMis
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:minuta
0

Zrobiłem kilka przeróbek i teraz nieblokujący stos jest u mnie lekko szybszy:

Kopiuj
package threads;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicReference;

public class ConcurrentStack {
    public static class BlockingStack<E> {

        private Node head = new Node();

        private volatile int size = 0;

        public synchronized void push(E e) {
            final Node newNode = new Node();
            newNode.value = e;
            newNode.next = head;
            head = newNode;
            size++;
        }

        public synchronized E pop() {
            final Node oldHead = head;
            if (oldHead == null) {
                return null;
            }
            head = oldHead.next;
            size--;
            return oldHead.value;
        }

        public int size() {
            return size;
        }

        class Node {
            E value;
            Node next;
        }
    }

    public static class NonBlockingStack<E>
            extends AtomicReference<NonBlockingStack<E>.Node> {
        public NonBlockingStack() {
            Node nil = new Node();
            nil.size = 0;
            set(nil);
        }

        public int size() {
            return get().size;
        }

        public void push(E e) {
            Node newNode = new Node();
            newNode.value = e;
            Node oldHead;
            do {
                oldHead = get();
                newNode.next = oldHead;
                newNode.size = oldHead.size + 1;
            } while (!compareAndSet(oldHead, newNode));
        }

        public E pop() {
            Node oldHead;
            Node newHead;

            do {
                oldHead = get();
                if (oldHead.size == 0) {
                    return null;
                }
                newHead = oldHead.next;
            } while (!compareAndSet(oldHead, newHead));
            return oldHead.value;
        }

        class Node {
            E value;
            Node next;
            int size;
        }
    }

    private static final int NUM_OF_THREADS = 8;
    private static final int OPERATIONS = 1_000_000;
    private static final Thread[] THREADS = new Thread[NUM_OF_THREADS];

    public static void main(String[] args) throws InterruptedException {
//        final BlockingStack<Integer> ints = new BlockingStack<>();
        final NonBlockingStack<Integer> ints = new NonBlockingStack<>();

        final CountDownLatch cdl = new CountDownLatch(NUM_OF_THREADS + 1);

        for (int i = 0; i < NUM_OF_THREADS; i++) {
            THREADS[i] = new Thread(() -> {
                try {
                    cdl.countDown();
                    cdl.await();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < OPERATIONS; j++) {
                    ints.push(j);
                }
            });
        }

        for (Thread t : THREADS) {
            t.start();
        }

        long start = System.nanoTime();
        cdl.countDown();
        for (Thread t : THREADS) {
            t.join();
        }
        long elapsed = System.nanoTime() - start;

        System.out.println("elapsed: " + elapsed);
        System.out.println("size: " + ints.size());

        long nsPerItem = elapsed / (NUM_OF_THREADS * (long) OPERATIONS);
        System.out.print("throughput: " + nsPerItem + " ns/item");
    }
}

W nieblokującym stosie size wstawiłem do Node żeby było poprawnie. Wcześniej mogła nastąpić utrata spójności danych jeśli np między head.compareAndSet i size.getAndIncrement zaszło przełączenie wątku.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 1x, ostatnio: Wibowit
Charles_Ray
W ConcurrentHashMapie size() również jest niespójny, coś za coś. Pytanie czy to źle :)
Wibowit
Pytanie czego autor oczekuje.
BM
  • Rejestracja:około 5 lat
  • Ostatnio:około 5 lat
  • Postów:3
0
Wibowit napisał(a):

Zrobiłem kilka przeróbek i teraz nieblokujący stos jest u mnie lekko szybszy:

No moze i jest lekko szybszy dla 1mln operacji, ale dla 100k blokujacy dalej jest wydajniejszy :D

Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:minuta
0

Możliwe, że dla 100k JVMka większość czasu spędza w interpreterze i to wpływa mocno na wyniki. Kod blokujący jest krótszy, a to jest ważne podczas interpretowania.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4706
3
  1. nie testujesz stosu - testujesz TYLKO operację push - to pierwszy problem
  2. Testowanie nie uwzględnia pułapek JVM - polecam użyć wzmiankowanego już JMH, inaczej możesz od razu wyniki wyrzucać na śmieci
  • w szczególnopści jak twoje 100k operacji trwa poniżej pół sekundy (za krótko) to w zasadzie cały powiar jest niszczony przez elementy spoza kodu:
    szybkośc Thead.join(), rozpędzanie JVM, wypadki gc itd.

Przetestuj porządnie na sensownym scenariuszu, mozesz robić 100k operacji, ale zrób też pop i powtórz je w petli 100 razy np. JMH robi takie rzeczy dla Ciebie. (plus dużo innych).
Poczytaj o blackHole - bo to (a w zasadzie brak) może całkiem niszczyć twoje pomiary.
(Poprawka: przez to, że czytasz size() - dodatkiowy blackhole nie jest CI potrzebny - przypadkiem akurat to jest dobrze :-) ).

JVM to świnia - szczególnie w benchmarkach - możesz zacząć tu : https://www.oracle.com/technical-resources/articles/java/architect-benchmarking.html, ale jest materiałów na miesiące uczenia (w tym temacie).


jeden i pół terabajta powinno wystarczyć każdemu
edytowany 8x, ostatnio: jarekr000000
BraVolt
  • Rejestracja:prawie 6 lat
  • Ostatnio:prawie 4 lata
  • Lokalizacja:Warszawa
  • Postów:2918
0

Na szybko, z blackhole i bez i dla porównania pusta matoda. Warto sobie puścić żeby się przekonać, że bez blackhole może być tak wydajnie jak pusta metoda.

Kopiuj
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.infra.Blackhole;

@State(Scope.Thread)
public class WithAndWithoutBlackhole {

    @Benchmark
    @Fork(1)
    @Warmup(iterations = 1)
    @Measurement(iterations = 1)
    public void measureEmptyMethod(Blackhole blackhole) {

    }

    @Benchmark
    @Fork(1)
    @Warmup(iterations = 1)
    @Measurement(iterations = 1)
    public void measureAddMethod(Blackhole blackhole) {

        int sum = 1 + 1;
//        blackhole.consume(sum);
    }

    @Benchmark
    @Fork(1)
    @Warmup(iterations = 1)
    @Measurement(iterations = 1)
    public void measureAddMethodWithALittleHelpFromBlackhole(Blackhole blackhole) {

        int sum = 1 + 1;
        blackhole.consume(sum);
    }
}

"Kiedy wiedzieć czy zacząć nauke Springa? bo w czystej Javie to nic ciekawego nie zrobie chyba"
Ein Volk, ein Reich, ein Kwa-Kwa ***** ***
jarekr000000
  • Rejestracja:ponad 8 lat
  • Ostatnio:około godziny
  • Lokalizacja:U krasnoludów - pod górą
  • Postów:4706
2

Przy okazji - lock free nie zawsze musi być szybsze. Na obecnym sprzęcie przeważnie będzie, ale jest trochę: to zależy, co robimy, ile rdzeni, jak często są kolizje.
W szczególności przydałoby się żebyś miał jakieś operacje typu READ (to nie jest konieczne, ale pomocne).
Mógłbyś dopsiac klasyczną operacje top, (oprócz pop, size i push), zapuścić jakiś ciekawszy scenariusz - gdzie wszystkie będą użyte, i wtedy zobacz.


jeden i pół terabajta powinno wystarczyć każdemu
BM
  • Rejestracja:około 5 lat
  • Ostatnio:około 5 lat
  • Postów:3
0

Ogólnie dzięki za odzew, sporo cennych informacji jeśli chodzi o wiarygodne mierzenie. Na pewno przyjrzę się JMH.
@jarekr000000 W mojej obecnej implementacji faktycznie - jeśli porobię jakieś dodatkowe jak np pop() to nieblokujacy stack jest szybszy. - Przy czym ważnym aspektem jest chyba LockSupport.parkNanos(1) w momencie gdy CAS sie nie udal. A wynika to z tego https://dzone.com/articles/wanna-get-faster-wait-bit

Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:minuta
0
BialyMis napisał(a):

Przy czym ważnym aspektem jest chyba LockSupport.parkNanos(1) w momencie gdy CAS sie nie udal. A wynika to z tego https://dzone.com/articles/wanna-get-faster-wait-bit

Wynik jednego benchmarka niekoniecznie jest ma przełożenie na inne obliczenia. Ogólna zasada z benchmarkami jest taka: jeśli chcesz osiągnąć wydajność jak w benchmarkach, pisz kod dokładnie taki jaki jest w benchmarkach. W benchmarku są idiotyczne obliczenia - cóż, jeśli chcesz mieć identyczną wydajność to musisz robić identycznie idiotyczne obliczenia. 1 nanosekunda postoju to strasznie mało. Jeśli masz coś wielokrotnie dłuższego do obliczenia to być może trzeba ten odstęp czasowy zwiększyć. Wszystko zależy od konkretnego przypadku. Najlepiej skup się na wąskich gardłach w rzeczywistych zastosowaniach niż na maksymalizowaniu wydajności w sztucznym i bezużytecznym kodzie.

Wracając do kombinacji z kodem to sprawy można też podejść inaczej. Zamiast ponawiać obliczenia można mieć spin-wait, tzn zamiast:

Kopiuj
volatile State state;
synchronized void next() {
    state = fn(state);
}

czy:

Kopiuj
final AtomicReference<State> state = ...;
void next() {
  state.updateAndGet(state -> fn(state));
}

Możesz dorobić coś w rodzaju semafora:

Kopiuj
volatile State state;
final AtomicBoolean permit = new AtomicBoolean(true);
void next() {
  while (!permit.compareAndSet(true, false)) {
    Thread.onSpinWait(); // dostępne od Javy 9
  }
  state = fn(state);
  permit.set(true);
}

Takie coś symuluje synchronized tylko zamiast usypiania wątku jest busy-loop.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 2x, ostatnio: Wibowit
BraVolt
  • Rejestracja:prawie 6 lat
  • Ostatnio:prawie 4 lata
  • Lokalizacja:Warszawa
  • Postów:2918
0
Wibowit napisał(a):

1 nanosekunda postoju to strasznie mało.

"Ziarnistość" czasu w JVM Windows to chyba ok. 300 ns (Unix, Linux trochę mniej).


"Kiedy wiedzieć czy zacząć nauke Springa? bo w czystej Javie to nic ciekawego nie zrobie chyba"
Ein Volk, ein Reich, ein Kwa-Kwa ***** ***
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:minuta
0
BraVolt napisał(a):
Wibowit napisał(a):

1 nanosekunda postoju to strasznie mało.

"Ziarnistość" czasu w JVM Windows to chyba ok. 300 ns (Unix, Linux trochę mniej).

Zegarów jest wiele rodzajów. System.nanoTime czy System.currentTimeMillis muszą być globalnie monotoniczne natomiast parkNanos teoretycznie nie musi spełniać takich wymagań.

Tak czy siak wątek z busy-loop czeka dokładnie tyle ile trzeba (o ile go system operacyjny nie wywłaszczy).


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
edytowany 2x, ostatnio: Wibowit
BraVolt
  • Rejestracja:prawie 6 lat
  • Ostatnio:prawie 4 lata
  • Lokalizacja:Warszawa
  • Postów:2918
0
Wibowit napisał(a):

Zegarów jest wiele rodzajów. System.nanoTime czy System.currentTimeMillis muszą być globalnie monotoniczne natomiast parkNanos teoretycznie nie musi spełniać takich wymagań.

Nie mam doświadczenia, pamiętam tylko z jakiejś prezentacji jug'owej, żeby czasu nie brać (najlepiej) poniżej nastu-mikrosekund. Implementacje, błąd pomiarowy itd.


"Kiedy wiedzieć czy zacząć nauke Springa? bo w czystej Javie to nic ciekawego nie zrobie chyba"
Ein Volk, ein Reich, ein Kwa-Kwa ***** ***
Wibowit
  • Rejestracja:prawie 20 lat
  • Ostatnio:minuta
1

Z eksperymentów tutaj https://hazelcast.com/blog/locksupport-parknanos-under-the-hood-and-the-curious-case-of-parking/ wynika iż okres parkowania na Linuksie jest domyślnie zaokrąglany w górę do wielokrotności aż 50 mikrosekund. Myślę, że warto sprawdzić jakie jest zużycie CPU przez poszczególne wątki za pomocą https://docs.oracle.com/javase/8/docs/api/java/lang/management/ThreadMXBean.html Być może to LockSupport.parkNanos sprawia, że wątki większość czasu śpią zamiast walczyć o wspólne zasoby.


"Programs must be written for people to read, and only incidentally for machines to execute." - Abelson & Sussman, SICP, preface to the first edition
"Ci, co najbardziej pragną planować życie społeczne, gdyby im na to pozwolić, staliby się w najwyższym stopniu niebezpieczni i nietolerancyjni wobec planów życiowych innych ludzi. Często, tchnącego dobrocią i oddanego jakiejś sprawie idealistę, dzieli od fanatyka tylko mały krok."
Demokracja jest fajna, dopóki wygrywa twoja ulubiona partia.
Kliknij, aby dodać treść...

Pomoc 1.18.8

Typografia

Edytor obsługuje składnie Markdown, w której pojedynczy akcent *kursywa* oraz _kursywa_ to pochylenie. Z kolei podwójny akcent **pogrubienie** oraz __pogrubienie__ to pogrubienie. Dodanie znaczników ~~strike~~ to przekreślenie.

Możesz dodać formatowanie komendami , , oraz .

Ponieważ dekoracja podkreślenia jest przeznaczona na linki, markdown nie zawiera specjalnej składni dla podkreślenia. Dlatego by dodać podkreślenie, użyj <u>underline</u>.

Komendy formatujące reagują na skróty klawiszowe: Ctrl+B, Ctrl+I, Ctrl+U oraz Ctrl+S.

Linki

By dodać link w edytorze użyj komendy lub użyj składni [title](link). URL umieszczony w linku lub nawet URL umieszczony bezpośrednio w tekście będzie aktywny i klikalny.

Jeżeli chcesz, możesz samodzielnie dodać link: <a href="link">title</a>.

Wewnętrzne odnośniki

Możesz umieścić odnośnik do wewnętrznej podstrony, używając następującej składni: [[Delphi/Kompendium]] lub [[Delphi/Kompendium|kliknij, aby przejść do kompendium]]. Odnośniki mogą prowadzić do Forum 4programmers.net lub np. do Kompendium.

Wspomnienia użytkowników

By wspomnieć użytkownika forum, wpisz w formularzu znak @. Zobaczysz okienko samouzupełniające nazwy użytkowników. Samouzupełnienie dobierze odpowiedni format wspomnienia, zależnie od tego czy w nazwie użytkownika znajduje się spacja.

Znaczniki HTML

Dozwolone jest używanie niektórych znaczników HTML: <a>, <b>, <i>, <kbd>, <del>, <strong>, <dfn>, <pre>, <blockquote>, <hr/>, <sub>, <sup> oraz <img/>.

Skróty klawiszowe

Dodaj kombinację klawiszy komendą notacji klawiszy lub skrótem klawiszowym Alt+K.

Reprezentuj kombinacje klawiszowe używając taga <kbd>. Oddziel od siebie klawisze znakiem plus, np <kbd>Alt+Tab</kbd>.

Indeks górny oraz dolny

Przykład: wpisując H<sub>2</sub>O i m<sup>2</sup> otrzymasz: H2O i m2.

Składnia Tex

By precyzyjnie wyrazić działanie matematyczne, użyj składni Tex.

<tex>arcctg(x) = argtan(\frac{1}{x}) = arcsin(\frac{1}{\sqrt{1+x^2}})</tex>

Kod źródłowy

Krótkie fragmenty kodu

Wszelkie jednolinijkowe instrukcje języka programowania powinny być zawarte pomiędzy obróconymi apostrofami: `kod instrukcji` lub ``console.log(`string`);``.

Kod wielolinijkowy

Dodaj fragment kodu komendą . Fragmenty kodu zajmujące całą lub więcej linijek powinny być umieszczone w wielolinijkowym fragmencie kodu. Znaczniki ``` lub ~~~ umożliwiają kolorowanie różnych języków programowania. Możemy nadać nazwę języka programowania używając auto-uzupełnienia, kod został pokolorowany używając konkretnych ustawień kolorowania składni:

```javascript
document.write('Hello World');
```

Możesz zaznaczyć również już wklejony kod w edytorze, i użyć komendy  by zamienić go w kod. Użyj kombinacji Ctrl+`, by dodać fragment kodu bez oznaczników języka.

Tabelki

Dodaj przykładową tabelkę używając komendy . Przykładowa tabelka składa się z dwóch kolumn, nagłówka i jednego wiersza.

Wygeneruj tabelkę na podstawie szablonu. Oddziel komórki separatorem ; lub |, a następnie zaznacz szablonu.

nazwisko;dziedzina;odkrycie
Pitagoras;mathematics;Pythagorean Theorem
Albert Einstein;physics;General Relativity
Marie Curie, Pierre Curie;chemistry;Radium, Polonium

Użyj komendy by zamienić zaznaczony szablon na tabelkę Markdown.

Lista uporządkowana i nieuporządkowana

Możliwe jest tworzenie listy numerowanych oraz wypunktowanych. Wystarczy, że pierwszym znakiem linii będzie * lub - dla listy nieuporządkowanej oraz 1. dla listy uporządkowanej.

Użyj komendy by dodać listę uporządkowaną.

1. Lista numerowana
2. Lista numerowana

Użyj komendy by dodać listę nieuporządkowaną.

* Lista wypunktowana
* Lista wypunktowana
** Lista wypunktowana (drugi poziom)

Składnia Markdown

Edytor obsługuje składnię Markdown, która składa się ze znaków specjalnych. Dostępne komendy, jak formatowanie , dodanie tabelki lub fragmentu kodu są w pewnym sensie świadome otaczającej jej składni, i postarają się unikać uszkodzenia jej.

Dla przykładu, używając tylko dostępnych komend, nie możemy dodać formatowania pogrubienia do kodu wielolinijkowego, albo dodać listy do tabelki - mogłoby to doprowadzić do uszkodzenia składni.

W pewnych odosobnionych przypadkach brak nowej linii przed elementami markdown również mógłby uszkodzić składnie, dlatego edytor dodaje brakujące nowe linie. Dla przykładu, dodanie formatowania pochylenia zaraz po tabelce, mogłoby zostać błędne zinterpretowane, więc edytor doda oddzielającą nową linię pomiędzy tabelką, a pochyleniem.

Skróty klawiszowe

Skróty formatujące, kiedy w edytorze znajduje się pojedynczy kursor, wstawiają sformatowany tekst przykładowy. Jeśli w edytorze znajduje się zaznaczenie (słowo, linijka, paragraf), wtedy zaznaczenie zostaje sformatowane.

  • Ctrl+B - dodaj pogrubienie lub pogrub zaznaczenie
  • Ctrl+I - dodaj pochylenie lub pochyl zaznaczenie
  • Ctrl+U - dodaj podkreślenie lub podkreśl zaznaczenie
  • Ctrl+S - dodaj przekreślenie lub przekreśl zaznaczenie

Notacja Klawiszy

  • Alt+K - dodaj notację klawiszy

Fragment kodu bez oznacznika

  • Alt+C - dodaj pusty fragment kodu

Skróty operujące na kodzie i linijkach:

  • Alt+L - zaznaczenie całej linii
  • Alt+, Alt+ - przeniesienie linijki w której znajduje się kursor w górę/dół.
  • Tab/⌘+] - dodaj wcięcie (wcięcie w prawo)
  • Shit+Tab/⌘+[ - usunięcie wcięcia (wycięcie w lewo)

Dodawanie postów:

  • Ctrl+Enter - dodaj post
  • ⌘+Enter - dodaj post (MacOS)