• Главная
  • Контакты
Подписаться:
  • Twitter
  • Facebook
  • RSS
  • VK
  • PushAll
logo

logo

  • Все
    • Положительные
    • Отрицательные
  • За сегодня
    • Положительные
    • Отрицательные
  • За вчера
    • Положительные
    • Отрицательные
  • За 3 дня
    • Положительные
    • Отрицательные
  • За неделю
    • Положительные
    • Отрицательные
  • За месяц
    • Положительные
    • Отрицательные
  • За год
    • Положительные
    • Отрицательные
  • Сортировка
    • По дате (возр)
    • По дате (убыв)
    • По рейтингу (возр)
    • По рейтингу (убыв)
    • По комментам (возр)
    • По комментам (убыв)
    • По просмотрам (возр)
    • По просмотрам (убыв)
Главная
  • Все
    • Положительные
    • Отрицательные
  • За сегодня
    • Положительные
    • Отрицательные
  • За вчера
    • Положительные
    • Отрицательные
  • За 3 дня
    • Положительные
    • Отрицательные
  • За неделю
    • Положительные
    • Отрицательные
  • За месяц
    • Положительные
    • Отрицательные
  • Главная
  • Как это работает в мире java. ConcurrentMap

Как это работает в мире java. ConcurrentMap +35

23.04.2017 18:11
kuptservol 3 10100 Источник
Java*

Основной принцип программирования гласит: не изобретать велосипед. Но иногда, чтобы понять, что происходит и как использовать инструмент правильно, нам необходимо это делать. Сегодня изобретаем ConcrurrentHashMap.


Сперва нам понадобятся 2 вещи. Начнем с 2х тестов — первый скажет, что у нашей реализации нет data races (на самом деле нам нужно проверить, правилен ли наш тест также путем тестирования заведомо некорректной реализации), второй тест мы будем использовать для тестирования производительности с точки зрения throughput.



Рассмотрим только несколько методов из интерфейса Map:


public interface Map<K, V> {
    V put(K key, V value);
    V get(Object key);
    V remove(Object key);
    int size();
}

Thread-safety correctness test


Практически невозможно написать тест безопасности потоков достаточно исчерпывающе, вам необходимо принять во внимание все аспекты, определенные в главе 17 JLS, более того, в большой степени тест зависит от модели аппаратной памяти или реализации JVM.


Для thread-safe correctness теста используем одну из готовых библиотек стресс-тестов, такую как jcstress, которая будет запускать ваш код, пытаясь найти несогласованность данных. Хотя jcstress все еще отмечен, как экспериментальный, это лучший выбор. Почему сложно написать собственный тест на параллелизм — посмотрите лекцию Шипилева.


Я использую для запуска jstress jcstress-gradle-plugin. Полный исходный код можно найти how-it-works-concurrent-map.


public class ConcurrentMapThreadSafetyTest {

    @State
    public static class MapState {
        final Map<String, Integer> map = new HashMap<>(3);
    }

    @JCStressTest
    @Description("Test race map get and put")
    @Outcome(id = "0, 1", expect = ACCEPTABLE, desc = "return 0L and 1L")
    @Outcome(expect = FORBIDDEN, desc = "Case violating atomicity.")
    public static class MapPutGetTest {

        @Actor
        public void actor1(MapState state, LongResult2 result) {
            state.map.put("A", 0);
            Integer r = state.map.get("A");
            result.r1 = (r == null ? -1 : r);
        }

        @Actor
        public void actor2(MapState state, LongResult2 result) {
            state.map.put("B", 1);
            Integer r = state.map.get("B");
            result.r2 = (r == null ? -1 : r);
        }
    }

    @JCStressTest
    @Description("Test race map check size")
    @Outcome(id = "2", expect = ACCEPTABLE, desc = "size of map = 2 ")
    @Outcome(id = "1", expect = FORBIDDEN, desc = "size of map = 1 is race")
    @Outcome(expect = FORBIDDEN, desc = "Case violating atomicity.")
    public static class MapSizeTest {

        @Actor
        public void actor1(MapState state) {
            state.map.put("A", 0);
        }

        @Actor
        public void actor2(MapState state) {
            state.map.put("B", 0);
        }

        @Arbiter
        public void arbiter(MapState state, IntResult1 result) {
            result.r1 = state.map.size();
        }
    }
}

В первом тесте MapPutGetTest у нас есть два потока, выполняющих одновременно методы actor1 и actor2, соответственно, оба они кладут некоторое значение в map и проверяют их обратно, если нет гонки данных, оба потока должны видеть заданные значения.


Во втором MapSizeTest мы одновременно помещаем два разных ключа в map и после проверки размера — если нет гонки данных — ожидаемый результат должен быть = 2.


Для того, чтобы проверить корректность теста, выполним его на заведомо потоконебезопасном HashMap — мы должны наблюдать нарушение атомарности. Если запустить тест на потокобезопасном ConcurrentHashMap — мы не должны видеть нарушение консистентности.


Результаты с HashMap:


[FAILED] ru.skuptsov.concurrent.map.test.ConcurrentMapTest.MapPutGetTest
  Observed state   Occurrences   Expectation     Interpretation                                              
           -1, 1         293,867        FORBIDDEN     Case violating atomic                                 
           0, -1         282,190        FORBIDDEN     Case violating atomic                              
            0, 1         28,013,763    ACCEPTABLE    return 0 and 1
[FAILED] ru.skuptsov.concurrent.map.test.ConcurrentMapTest.MapSizeTest
  Observed state   Occurrences   Expectation     Interpretation                                              
               1          1,434,783      FORBIDDEN    size of map = 1 race                                
               2          11,733,097    ACCEPTABLE   size of map = 2

В потоконебезопасном HashMap мы видим некоторое статистическое количество несогласованных результатов, оба тесты не прошли.


Результаты с потокобезопасными ConcurrentHashMap:


[OK] ru.skuptsov.concurrent.map.test.ConcurrentMapTest.MapPutGetTest
  Observed state   Occurrences   Expectation    Interpretation                                              
            0, 1         20,195,000     ACCEPTABLE
[OK] ru.skuptsov.concurrent.map.test.ConcurrentMapTest.MapSizeTest
  Observed state   Occurrences   Expectation    Interpretation                                              
               2          6,573,730      ACCEPTABLE  size of map = 2

ConcurrentHashMap прошел тест, по крайней мере мы можем признать, что наш тест может обнаружить некоторые простые проблемы параллелизма. Такие же результаты можно проверить и для Collection.synchronizedMap и HashTable.


Fitst ConcurrentHashMap attempt


Первый наивный подход — просто синхронизировать каждый доступ к внутренним структурам — массиву бакетов.


Фактически, мы можем написать некоторую параллельную оболочку над переданным map провайдером. Точно так же действует java.util.Collections.synchronizedMap, Hashtable и гуавовский synchronizedMultimap.


public class SynchrinizedHashMap<K, V> extends BaseMap<K, V> implements Map<K, V>, IMap<K, V> {

    private final Map<K, V> provider;
    private final Object monitor;

    public SynchronizedHashMap(Map<K, V> provider) {
        this.provider = provider;
        monitor = this;
    }

    @Override
    public V put(K key, V value) {
        synchronized (monitor) {
            return provider.put(key, value);
        }
    }

    @Override
    public V get(Object key) {
        synchronized (monitor) {
            return provider.get(key);
        }
    }

    @Override
    public int size() {
        synchronized (monitor) {
            return provider.size();
        }
    }
}

Изменения в non-volatile map-провайдере будут видны между потоками, согласно документации:


Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized method for the same object. This guarantees that changes to the state of the object are visible to all threads.

Наша простейшая реализация проходит параллельные тесты, но по какой цене? В каждом методе может быть только один поток в одно и то же время, даже если мы работаем с разными ключами, поэтому при многопоточной нагрузке мы не должны ожидать высокой производительности. Давайте измерим это.


Тест производительности


Для тестирования производительности мы будем использовать библиотечку jmh.


@State(Scope.Thread)
@Warmup(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 5, time = 1, timeUnit = TimeUnit.SECONDS)
@Fork(3)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(MICROSECONDS)
public class ConcurrentMapBenchmark {
    private Map<Integer, Integer> map;

    @Param({"concurrenthashmap", "hashtable", "synchronizedhashmap"})
    private String type;

    @Param({"1", "10"})
    private Integer writersNum;

    @Param({"1", "10"})
    private Integer readersNum;

    private final static int NUM = 1000;

    @Setup
    public void setup() {
        switch (type) {
            case "hashtable":
                map = new Hashtable<>();
                break;
            case "concurrenthashmap":
                map = new ConcurrentHashMap<>();
                break;
            case "synchronizedhashmap":
                map = new SynchronizedHashMap<>(new HashMap<>());
                break;
        }
    }

    @Benchmark
    public void test(Blackhole bh) throws ExecutionException, InterruptedException {

        List<CompletableFuture> futures = new ArrayList<>();

        for (int i = 0; i < writersNum; i++) {
            futures.add(CompletableFuture.runAsync(() -> {
                for (int j = 0; j < NUM; j++) {
                    map.put(j, j);
                }
            }));
        }

        for (int i = 0; i < readersNum; i++) {
            futures.add(CompletableFuture.runAsync(() -> {
                for (int j = 0; j < NUM; j++) {
                    bh.consume(map.get(j));
                }
            }));
        }

        CompletableFuture.allOf(futures.toArray(new CompletableFuture[1])).get();
    }
}

image
Мы убедились, что производительность нашей SynchronizedHashMap практически схожа с java-s HashTable, и она в 2 раза хуже, чем ConcurrentHashMap. Попробуем улучшить производительность.


Lock-striping ConcurrentHashMap attempt


Первое улучшение может быть основано на идее, что вместо блокирования доступа ко всей map лучше синхронизовать доступ только если потоки обращаются к одному и тому же бакету, где индекс бакета = key.hashCode ()% array.length. Этот метод называется lock striping или fine-grained synchronization, см. Искусство многопроцессорного программирования.


Для массива бакетов нам понадобится массив блокировок, при старте размер массива блокировок должен быть равен внутреннему размеру массива — это важно, потому что мы не хотим ситуации, когда 2 locka отвечают за один бакет массива.


Для простоты рассмотрим map с неизменяемым массивом бакетов — это означает, что мы не сможем расширить начальную емкость (если N >> initialCapacity мы потеряем O (1) map гарантию вставки доставания элементов. Также нам не нужен loadFactor). Расширяемая cocurrent map это отдельная большая тема.


public class LockStripingArrayConcurrentHashMap<K, V> extends BaseMap<K, V> implements Map<K, V> {

    private final AtomicInteger count = new AtomicInteger(0);
    private final Node<K, V>[] buckets;
    private final Object[] locks;

    @SuppressWarnings({"rawtypes", "unchecked"})
    public LockStripingArrayConcurrentHashMap(int capacity) {
        locks = new Object[capacity];
        for (int i = 0; i < locks.length; i++) {
            locks[i] = new Object();
        }

        buckets = (Node<K, V>[]) new Node[capacity];
    }

    @Override
    public int size() {
        return count.get();
    }

    @Override
    public V get(Object key) {
        if (key == null) throw new IllegalArgumentException();
        int hash = hash(key);
        synchronized (getLockFor(hash)) {
            Node<K, V> node = buckets[getBucketIndex(hash)];

            while (node != null) {
                if (isKeyEquals(key, hash, node)) {
                    return node.value;
                }

                node = node.next;
            }

            return null;
        }
    }

    @Override
    public V put(K key, V value) {
        if (key == null || value == null) throw new IllegalArgumentException();
        int hash = hash(key);
        synchronized (getLockFor(hash)) {
            int bucketIndex = getBucketIndex(hash);
            Node<K, V> node = buckets[bucketIndex];

            if (node == null) {
                buckets[bucketIndex] = new Node<>(hash, key, value, null);
                count.incrementAndGet();
                return null;
            } else {
                Node<K, V> prevNode = node;
                while (node != null) {
                    if (isKeyEquals(key, hash, node)) {
                        V prevValue = node.value;
                        node.value = value;

                        return prevValue;
                    }

                    prevNode = node;
                    node = node.next;
                }

                prevNode.next = new Node<>(hash, key, value, null);
                count.incrementAndGet();
                return null;
            }          ...
        }
    }

    private boolean isKeyEquals(Object key, int hash, Node<K, V> node) {
        return node.hash == hash &&
                node.key == key ||
                (node.key != null && node.key.equals(key));
    }

    private int hash(Object key) {
        return key.hashCode();
    }

    private int getBucketIndex(int hash) {
        return hash % buckets.length;
    }

    private Object getLockFor(int hash) {
        return locks[hash % locks.length];
    }

    private static class Node<K, V> {
        final int hash;
        K key;
        V value;
        Node<K, V> next;

        Node(int hash, K key, V value, Node<K, V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
    }
}

Важно, чтобы все поля класса были final — это гарантирует safe-publication и что никто не вызовет методы до окончательного создания объекта — нам это важно, потому что у нас есть некая инициализация в конструкторе.


Исходный код можно найти здесь.


Результаты тестов:
image
 
Мы видим, что fine-grained synchronization реализация лучше, чем общая блокировка. Результаты, при одном читателе и одном писателе, по сравнению с ConcurrentHashMap практически одинаковы, но когда число потоков увеличивается, разница больше, особенно там, где много читателей.


Lock free concurrent hash map attempt


Честно говоря, синхронизация не является методом параллельного программирования, потому что она выставляет потоки в последовательной очереди, заставляя ждать завершения другого потока. И дополнительная стоимость синхронизации контекста системы возрастает с увеличением числа ожидающих потоков, но все, что мы хотим сделать — это небольшое количество инструкций для изменения значения ключа map.


Определим некоторые требования к новой реализации hashmap, которые по идее должны улучшить нашу реализацию. И требования следующие:


  1. Если мы имеем 2 потока, которые работают с разными ключами (запись или чтение), мы не хотим какой-либо синхронизации между ними (word tearing в java запрещен — доступ к двум различным полям массива потоко-безопасен)
  2. Если несколько потоков работают с одним и тем же ключом (запись и чтение), мы не хотим реордеринга операций (подробнее о причинах проблем в структуре современого кэша) и нуждаемся в happens-before гарантии между потоками, иначе один поток может не заметить измененное значение другим потоком. Но мы не хотим блокировать поток чтения и ждать завершения потока записи.
  3. Мы не хотим блокировать несколько читателей по одному ключу, если среди них нет одного пишущего потока.

Давайте сконцентрируемся на пунктах 2 и 3. На самом деле мы можем сделать операцию чтения map полностью свободной от блокировки, если мы сможем сделать (1) volatile read array of buckets, а затем пройти внутри bucket-а с (2) volatile read следующего узла связанного списка пока не найдем нужный и volatile read самого значения узла.


Для (2) мы можем просто пометить в Node поля next и value как volatile.


Для (1) не существует такой вещи, как volatile array, даже если массив объявлен ??как volatile, это не обеспечивает volatile семантику при чтении или записи элементов, при одновременном доступе к k-му элементу массива требуется внешняя синхронизация, volatile является только сама ссылка на массив. Мы можем использовать AtomicReferenceArray для этой цели, но он принимает только массивы Object[]. В качестве альтернативы рассмотрим использование Unsafe для volatile array read и lock-free write. Тот же метод используется в AtomicReferenceArray и ConcurrentHashMap.


@SuppressWarnings("unchecked")
// read array value by index
private <K, V> Node<K, V> volatileGetNode(int i) {
    return (Node<K, V>) U.getObjectVolatile(buckets, ((long) i << ASHIFT) + ABASE);
}

// cas set array value by index
private <K, V> boolean compareAndSwapNode(int i, Node<K, V> expectedNode, Node<K, V> setNode) {
    return U.compareAndSwapObject(buckets, ((long) i << ASHIFT) + ABASE, expectedNode, setNode);
}

private static final sun.misc.Unsafe U;
// Node[] header shift
private static final long ABASE;
// Node.class size shift
private static final int ASHIFT;

static {
    try {
// get unsafe by reflection - it is illegal to use not in java lib        Constructor<Unsafe> unsafeConstructor = Unsafe.class.getDeclaredConstructor();
        unsafeConstructor.setAccessible(true);
        U = unsafeConstructor.newInstance();
    } catch (NoSuchMethodException | InstantiationException | InvocationTargetException | IllegalAccessException e) {
        throw new RuntimeException(e);
    }

    Class<?> ak = Node[].class;

    ABASE = U.arrayBaseOffset(ak);
    int scale = U.arrayIndexScale(ak);
    ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
}

В volatile getNode мы теперь можем безопасно без блокировок читать значения.


Давайте теперь напишем lock-free V get (Object key):


public V get(Object key) {
    if (key == null) throw new IllegalArgumentException();
    int hash = hash(key);
    Node<K, V> node;

    // volatile read of bucket head at hash index
    if ((node = volatileGetNode(getBucketIndex(hash))) != null) {
        // check first node
        if (isKeyEquals(key, hash, node)) {
            return node.value;
        }

        // walk through the rest to find target node
        while ((node = node.next) != null) {
            if (isKeyEquals(key, hash, node))
                return node.value;
        }
    }

    return null;
}

В первой попытке был большой memory-overhead с пулом блокировок — на самом деле мы можем использовать тот же fine-grained подход без дополнительной памяти — просто заблокироваться на первом node в бакете, если он существует. Если он не существует — мы не можем блокироваться на несуществующем элеменет и нуждаемся в некотором lock-free методе для установки header node — мы уже написали этот метод выше — метод compareAndSwapNode.


@Override
public V put(K key, V value) {
    if (key == null || value == null) throw new IllegalArgumentException();
    int hash = hash(key);
    // no resize in this implementation - so the index will not change
    int bucketIndex = getBucketIndex(hash);

    // cas loop trying not to miss  
    while (true) {
        Node<K, V> node;
        // if bucket is empty try to set new head with cas
        if ((node = volatileGetNode(bucketIndex)) == null) {
            if (compareAndSwapNode(bucketIndex, null,
                    new Node<>(hash, key, value, null))) {
                // if we succeed to set head - then break and return null

                count.increment();
                break;
            }
        } else {
            // head is not null - try to find place to insert or update under lock
            synchronized (node) {
                // check if node have not been changed since we got it
                // otherwise let's go to another loop iteration
                if (volatileGetNode(bucketIndex) == node) {
                    V prevValue = null;
                    Node<K, V> n = node;
                    while (true) {
                        ... simply walk through list under lock and update or insert value... 
                    }

                    return prevValue;
                }
            }
        }
    }

    return null;
}

Полный исходный код здесь.


Давайте протестируем его производительность:
image


В некоторых случаях мы даже лучше, чем ConcurrentHashMap, но это не совсем честное сравнение. Потому что ConcurrentHashMap делает ленивую инициализацию таблицы во время загрузки и по крайней мере один раз происходит resize на граничном элементе threshold=initialCapacity*loadFactor. Если мы снова запустим тест с инициализированными элементами initialCapacity! = N (= N / 6), результаты будут несколько отличаться:


image


Это произошло из-за того, что в ConcurrentHashMap происходит увеличение начального размера массива бакетов и на получение элементов по ключу тратится меньше времени, из-за уменьшения длинны связного списка в бакете.


Нужно отметить, что мы получили не full-non-bloking структуру данных — так же, как и ConcurrentHashMap, хотя все, что нам нужно, — это просто связанный список без блокировок, но с изменением размера и одновременным модифицированием данных эта задача не такая простая — читайте здесь.


Оригинальная java 8 ConcurrentHashMap имеет ряд улучшений, о которых мы не упоминали, например:


  1. Ленивая инициализация таблицы бакетов, которая минимизирует memory footprint до первого использования
  2. Concurrent resizing массива бакетов
  3. Подсчет элементов с использованием LongAdder.
  4. Специальные типы nodes (начиная с 1.8) — TreeBins, если длина списка внутри бакета вырастет больше, чем TREEIFY_THRESHOLD = 8 — бакет становится сбалансированным деревом с наихудшим поиском по ключу (O (log (Nbucket_size)))

Нужно отметить, что реализация ConcurrentHashMap в Java 1.8 была существенно изменена с 1.7. В 1.7 это была идея сегментов, где число сегментов равно уровню параллелизма. В java 8 массив бакетов представляет собой единый массив.

Поделиться с друзьями
-->

Комментарии (3)


  1. sshikov
    23.04.2017 21:42
    #10187698
    +3

    В качестве альтернативы рассмотрим использование Unsafe для volatile array read и lock-free write. Тот же метод используется в AtomicReferenceArray и ConcurrentHashMap.

    Эк вы бодро так скипнули описание сути метода ) Тут было бы неплохо хоть два слова сказать, что именно вы используете из Unsafe, и почему/как это решает проблему отсутствия volatile для массивов. А то это далеко не очевидная штука, далеко не каждый пользовался Unsafe хотя бы раз в жизни.


    1. lany
      24.04.2017 17:53
      #10188984
      +5

      Вообще можно было и на VarHandle написать, Unsafe как-то выходит из моды ;-)


  1. lany
    24.04.2017 17:58
    #10188996

    Тесты производительности замеряют не только собственно производительность, но и создание/планировку/сбор результатов CompletableFuture. Пробовали @Group, @GroupThreads?

МЕТКИ

  • Хабы
  • Теги

JAVA

java

concurrency

hashmap

СЕРВИСЫ
  • logo

    CloudLogs.ru - Облачное логирование

    • Храните логи вашего сервиса или приложения в облаке. Удобно просматривайте и анализируйте их.
Все публикации автора
  • Как это работает в мире java. ConcurrentMap +35

    • 23.04.2017 18:11

    Как это работает в мире java. Пул потоков +9

    • 11.04.2017 18:48

Подписка


ЛУЧШЕЕ

  • Сегодня
  • Вчера
  • Позавчера
10:32

Наше расследование: ищем отечественные микросхемы в «отечественных» счетчиках электроэнергии. Часть 4 и снова блогер… +47

08:05

Оживляем топливомер из кабины «Боинга» +31

09:24

Diplodoc 5.0: как ускорить сборку документации в пять раз +23

09:01

TIG сварка в домашней мастерской, начало. Обзор, подборка мелочей. Часть 2 +23

10:53

Как организовать идеальное рабочее место: проверенные решения от команды Selectel +20

08:04

Безумный эксперимент: запускаем GTA V на Pentium 4 — возможно ли это? +19

13:05

Мой начальник хочет no-code в проде. Я против — и готов уйти +16

12:42

Программируя с использованием AI ты продаешь душу дьяволу +15

08:53

Что происходит с вашим JavaScript-кодом внутри V8. Часть 1 +14

11:08

Почему джуны — это инвестиция в команду, а не слабое звено? +12

13:01

Как я «случайно» получил root-доступ к платёжному терминалу +11

07:52

Домашняя мастерская по ремонту электроники: работа с ЛБП, мультиметром и осциллографом +11

08:31

«Разработчик – легенда»: анатомия волчистости в IT +10

11:27

Цветовая вычислительная фотография. Часть 2: Стандарты CIE 1931 +8

12:00

Nokia 6555: раскладушка, которая зажигала в 2000-х. Что внутри? +7

10:22

10 наивных советов тем, кто только начинает работать +7

09:32

Там, где метрики молчат: как расшифровка звонков помогла лучше понимать бизнес-клиентов +7

07:45

Как прямая помогает обучать машины +7

05:51

Почему sync.Map — почти всегда плохая идея +7

10:51

Нашел, проверил, убедил: как мы организовали генерацию SQL-запросов, проверку сложных данных и при чем здесь Allure +6

07:00

Редизайн Яндекс Карт: почему мы перекрасили дороги +177

08:05

Газоразрядное табло для машины времени, или как я оказался в титрах к японской дораме +115

13:01

Мирный порох +74

07:31

Как ускорить сложение и вычитание при помощи 2^51 +56

15:01

Мифы цифровой революции: почему гиперлупы не летают, а ИИ не правит миром (пока что) +50

12:01

Почему мы до сих пор пользуемся QWERTY: история самой неэффективной раскладки +49

04:53

Закат инженерной науки и что бы я посоветовал молодым людям, которые мечтают стать инженерами? +42

09:01

Просто редчайшая ГДР-овская Musima или уникальная мастеровая электрогитара из СССР? +40

13:00

Бизнесу не нужно внедрять ИИ. Рассказываю, как ИИ-хайп ослепил российские компании +35

09:01

Как СМИ, консультанты, инфоцыгане и прочие провоцируют переработки и корпоративную шизу. Часть 4 +31

08:05

Edge AI: локальный инференс — новый драйвер эффективности бизнеса +31

06:04

Это личное! Как femtech-приложения защищают наши данные +28

09:55

Проблемы БД или почему большой продакшн спасут только массовые расстрелы запросов +26

08:12

Как устроены фотонные компьютеры +25

07:51

ZLinq — Zero-Allocation LINQ-библиотека для.NET +25

10:24

Данные на продажу: что происходит с информацией после утечек +24

08:09

«Кобра»: персоналка эпохи социализма, о которой вы не знали +23

06:08

Исчисление геометрии Часть 1. Алгебры Клиффорда +21

08:30

3D для каждого. Оптимизация. Часть 4. Ремейк меша +20

08:01

Сложный способ писать программы +20

19:25

Localhost-атака: как Meta и Яндекс следят за пользователями Android через localhost +240

21:57

Как инфоцыгане отравили IT +228

13:01

Апгрейды для Денди: часть 2/2 +77

13:39

Прогрессивный JSON +45

09:01

Электронная нагрузка для разряда аккумуляторов на микроконтроллере PIC16F628A +45

08:14

Paranoia Mode: подборка инструментов для приватной и безопасной работы в Linux +41

08:23

3D-сканер из датчика Kinect Xbox 360 +33

12:55

Секреты изнанки музыкальных инструментов +32

12:02

Взломают или нет? Оцениваем риски вашей информационной системы и моделируем угрозы +31

10:38

Достижения российских ученых в первой половине 2025 года +29

08:05

Переводим спортивное табло на управление по Bluetooth и контроллер arduino +27

11:31

Один на один с Rust +24

11:35

Об (отсутствии) синтаксической поддержки обработки ошибок в Go +23

15:30

Из декрета в аналитику — как я вкатилась в IT +22

12:48

Философствующий Claude 4, Gemini для самых маленьких и пачка агентов-программистов: главные события мая в ИИ +22

12:15

Как построить свою ферму устройств и упростить работу с устройствами и эмуляторами: делимся опытом создания DeviceHub +22

07:19

5 игровых проектов, которые разрушают ожидания за 15 минут. И это прекрасно +19

15:00

Маленькая утилита для контроля квот в Yandex Cloud +17

09:45

Рецензия на книгу “Машинное обучение для приложений высокого риска” +15

02:09

LLM работают лучше если им угрожать? Вообще не факт +15

ОБСУЖДАЕМОЕ

  • Закат инженерной науки и что бы я посоветовал молодым людям, которые мечтают стать инженерами? +42

    • 401   43000

    Как инфоцыгане отравили IT +223

    • 287   42000

    Редизайн Яндекс Карт: почему мы перекрасили дороги +177

    • 267   33000

    Localhost-атака: как Meta и Яндекс следят за пользователями Android через localhost +240

    • 175   33000

    «Разработчик – легенда»: анатомия волчистости в IT +10

    • 78   6800

    Об (отсутствии) синтаксической поддержки обработки ошибок в Go +23

    • 56   4000

    Почему мы до сих пор пользуемся QWERTY: история самой неэффективной раскладки +49

    • 46   10000

    Образцовый джун +17

    • 46   3700

    Мирный порох +74

    • 42   4900

    Бизнесу не нужно внедрять ИИ. Рассказываю, как ИИ-хайп ослепил российские компании +35

    • 42   5600

    Конец эпохи программистов? Почему 80% IT-школ закроются к 2027 году -11

    • 36   2000

    Прогрессивный JSON +45

    • 32   13000

    Мифы цифровой революции: почему гиперлупы не летают, а ИИ не правит миром (пока что) +50

    • 31   4100

    Мой начальник хочет no-code в проде. Я против — и готов уйти +16

    • 30   1800

    LLM работают лучше если им угрожать? Вообще не факт +15

    • 29   2300
  • Главная
  • Контакты
© 2025. Все публикации принадлежат авторам.