Могу ли я легко написать программу для использования с процессором Intel Quad Core или i7, если используется только 1 поток? - PullRequest
2 голосов
/ 18 мая 2009

Интересно, если в моей программе есть только 1 поток, могу ли я написать его так, чтобы ядро ​​Quad или i7 действительно могли использовать разные ядра? Обычно, когда я пишу программы на четырехъядерном компьютере, загрузка ЦП будет только около 25%, и работа, похоже, будет разделена между 4 ядрами, как показывает диспетчер задач. (программы, которые я писал, обычно написаны на Ruby, Python или PHP, поэтому они могут быть не настолько оптимизированы).

Обновление: что, если я вместо этого напишу в C или C ++, и

for (i = 0; i < 100000000; i++) {
  a = i * 2;
  b = i + 1;
  if (a == ...  || b == ...) { ... }
}

, а затем используйте самый высокий уровень оптимизации с компилятором. может ли компилятор заставить умножение происходить на одном ядре, а сложение - на другом ядре, и, следовательно, заставить 2 ядра работать одновременно? Разве это не простая оптимизация для использования двух ядер?

Ответы [ 10 ]

7 голосов
/ 18 мая 2009

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

3 голосов
/ 18 мая 2009

Во-первых, если в программе не создано несколько потоков, то в этой программе есть только один поток выполнения.

То, что 25% ресурсов ЦП используется для программы, свидетельствует о том, что одно ядро ​​из четырех используется на 100%, но все остальные ядра не используются. Если бы использовались все ядра, то теоретически процесс мог бы использовать 100% ресурсов ЦП.

В качестве примечания, графики, показанные в диспетчере задач в Windows, показывают загрузку ЦП всеми процессами, запущенными в данный момент, а не только для одного процесса.

Во-вторых, код, который вы представляете, может быть разбит на код, который может выполняться в двух отдельных потоках для выполнения в двух ядрах. Я предполагаю, что вы хотите показать, что a и b не зависят друг от друга и зависят только от i. В такой ситуации разделение внутри цикла for, как показано ниже, может позволить многопоточную работу, которая может привести к повышению производительности:

// Process this in one thread:
for (int i = 0; i < 1000; i++) {
    a = i * 2;
}

// Process this in another thread:
for (int i = 0; i < 1000; i++) {
    b = i + 1;
}

Однако, что становится хитрым, так это то, что должно быть время, когда необходимо оценить результаты двух отдельных потоков, что, как кажется, подразумевается в последующем утверждении if:

for (i = 0; i < 1000; i++) {
  // manipulate "a" and "b"
  if (a == ...  || b == ...) { ... }
}

Для этого потребуется поиск значений a и b, которые находятся в отдельных потоках (которые выполняются на отдельных процессорах), что является серьезной головной болью.

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

Поэтому приведенный вами пример кода можно распараллелить только частично без особых усилий, однако, как только возникает необходимость сравнить две переменные, разделение этих двух операций становится очень трудным очень быстро.

Несколько правил, когда речь идет о параллельном программировании:

Если есть задачи, которые можно разбить на части, включающие обработку данных, полностью независимую от других данных и их результатов (состояний), то распараллеливание может быть очень простым.

Например, две функции, которые вычисляют значение из ввода (в псевдокоде):

f(x) = { return 2x }
g(x) = { return x+1 }

Эти две функции не зависят друг от друга, поэтому они могут выполняться параллельно без какой-либо боли. Кроме того, поскольку они не являются состояниями для совместного использования или обработки между вычислениями, даже если было несколько значений x, которые необходимо вычислить, даже эти можно разделить дальше:

x = [1, 2, 3, 4]
foreach t in x:
    runInThread(f(t))
foreach t in x:
    runInThread(g(t))

Теперь в этом примере мы можем иметь 8 отдельных потоков, выполняющих вычисления. Отсутствие побочных эффектов может быть очень хорошей вещью для параллельного программирования.

Однако, как только возникает зависимость от данных и результатов других вычислений (что также означает наличие побочных эффектов), распараллеливание становится чрезвычайно трудным. Во многих случаях эти типы проблем должны выполняться последовательно, поскольку они ожидают результатов других вычислений, которые будут возвращены.

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

2 голосов
/ 17 октября 2009

Я очень хорошо знаю чипы Intel.

Согласно вашему коду, «if (a == ... || b == ...)» является барьером, в противном случае ядра процессора будут выполнять весь код параллельно, независимо от того, какой компилятор выполнил какую оптимизацию. Это только требует, чтобы компилятор не был очень «глупым». Это означает, что аппаратное обеспечение имеет саму возможность, а не программное обеспечение. Так что многопоточное программирование или OpenMP в таких случаях не требуется, хотя они помогут улучшить параллельные вычисления. Обратите внимание, что здесь не подразумевается Hyper-Threading, просто обычные функции многоядерного процессора.

Чтобы узнать больше, пожалуйста, загляните в "параллельный многопроцессорный конвейер процессора".

Здесь я хотел бы привести классический пример, который мог бы выполняться параллельно многоядерными / многоканальными платформами IMC (например, семейством Intel Nehalem, такими как Core i7), без дополнительной оптимизации программного обеспечения.

char buffer0[64];
char buffer1[64];
char buffer2[64];
char buffer[192];

int i;
for (i = 0; i < 64; i++) {
    *(buffer + i) = *(buffer0 + i);
    *(buffer + 64 + i) = *(buffer1 + i);
    *(buffer + 128 + i) = *(buffer2 + i);
}

Почему? 3 причины.

1 Core i7 имеет трехканальный IMC, ширина шины составляет 192 бита, 64 бита на канал; и адресное пространство памяти чередуется между каналами на основе каждой строки кэша. Длина строки кэша составляет 64 байта. поэтому в основном buffer0 находится на канале 0, buffer1 будет на канале, а buffer2 на канале 2; в то время как для буфера [192], он чередовался между 3 каналами, по 64 на канал. IMC поддерживает одновременную загрузку или хранение данных из или на несколько каналов. Это многоканальный пакет MC с максимальной пропускной способностью. Хотя в моем следующем описании я скажу только 64 байта на канал, скажем, w / BL x8 (длина пакета 8, 8 x 8 = 64 байта = строка кэша) на канал.

2 buffer0..2 и buffer являются непрерывными в области памяти (на конкретной странице как виртуально, так и физически, стека memroy). при запуске buffer0, 1, 2 и buffer загружаются / выбираются в кэш процессора, всего 6 строк кэша. поэтому после начала выполнения вышеупомянутого кода «for () {}» доступ к памяти вообще не требуется, поскольку все данные находятся в кеше, кеше L3, неосновной части, которая используется всеми ядрами. Мы не будем говорить о L1 / 2 здесь. В этом случае каждое ядро ​​может собирать данные и затем вычислять их независимо, единственное требование состоит в том, чтобы ОС поддерживала MP, и была разрешена задача кражи, например, планирование времени выполнения и совместное использование сходств.

3 нет никаких зависимостей между buffer0, 1, 2 и buffer, так что нет никаких задержек выполнения или барьеров. например execute * (buffer + 64 + i) = * (buffer1 + i) не должен ждать выполнения * (buffer + i) = * (buffer0 + i) для завершения.

Хотя самый важный и сложный момент - это «кража задачи, планирование во время выполнения и совместное использование аффинности», потому что для заданной задачи есть только один контекст исключения задачи, и он должен совместно использоваться всеми ядрами для выполнения параллельного выполнения. Любой, кто мог бы понять этот момент, он / она является одним из лучших экспертов в мире. Я ищу такого эксперта, который бы работал над моим проектом с открытым исходным кодом и отвечал за параллельные вычисления и последние работы, связанные с архитектурами HPC.

Обратите внимание, что в приведенном выше примере кода вы также можете использовать некоторые инструкции SIMD, такие как movntdq / a, которые будут обходить кэш процессора и напрямую записывать память. Это также очень хорошая идея, когда выполняется оптимизация на уровне программного обеспечения, хотя доступ к памяти является чрезвычайно дорогим, например, для доступа к кэш-памяти (L1) может потребоваться всего лишь 1 цикл, а для доступа к памяти требуется 142 цикла на предыдущих чипах x86.

Пожалуйста, посетите http://effocore.googlecode.com и http://effogpled.googlecode.com, чтобы узнать подробности.

1 голос
/ 17 октября 2009

Если вы хотите параллельно выбрать «i», которые оценивают как «true» ваше утверждение if (a == ... || b == ...), то вы можете сделать это с помощью PLINQ (в .NET 4.0):

        //note the "AsParallel"; that's it, multicore support.
        var query = from i in Enumerable.Range(0, 100000000).AsParallel()
                    where (i % 2 == 1 && i >= 10) //your condition
                    select i;

        //while iterating, the query is evaluated in parallel! 
        //Result will probably never be in order (eg. 13, 11, 17, 15, 19..)
        foreach (var selected in query)
        {
            //not parallel here!
        }

Если вместо этого вы хотите распараллелить операции, вы сможете выполнить:

Parallel.For(0, 100000000, i =>
{
    if (i > 10)           //your condition here
        DoWork(i);        //Thread-safe operation
});
1 голос
/ 19 мая 2009

С C / C ++ вы можете использовать OpenMP . Это код C с прагмами вроде

#pragma omp parallel for
for(..) {
...
}

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

1 голос
/ 18 мая 2009

Единственный способ использовать несколько ядер без использования многопоточности - это использовать несколько программ.

В приведенном выше примере одна программа может обрабатывать 0-2499999, следующую 2500000-4999999 и так далее. Отключите все четыре из них одновременно, и они будут использовать все четыре ядра.

Обычно вам лучше написать (одну) многопоточную программу.

1 голос
/ 18 мая 2009

Однопоточная программа будет использовать только одно ядро. Операционная система вполне может решить время от времени переносить программу между ядрами - в соответствии с некоторыми правилами для балансировки нагрузки и т. Д. Таким образом, вы увидите, что в целом будет использоваться только 25%, а все четыре ядра работают - но только одно одновременно. 1001 *

1 голос
/ 18 мая 2009

Если код вашего приложения является однопоточным, несколько процессоров / ядер будут использоваться, только если:

  • используемые вами библиотеки используют несколько потоков (возможно, скрывая это использование за простым интерфейсом)
  • ваше приложение порождает другие процессы для выполнения некоторой части своей операции

Однако все приложения на Ruby, Python и PHP могут быть написаны для использования нескольких потоков.

1 голос
/ 18 мая 2009

Неявный параллелизм , вероятно, то, что вы ищете.

0 голосов
/ 18 мая 2009

Поскольку вы говорите о «диспетчере задач», похоже, вы работаете в Windows. Однако, если вы используете там веб-сервер (для Ruby или PHP с предварительным разветвлением fcgi или Apache, в меньшей степени, для других работников Apache), с несколькими процессами, то они будут распространяться по ядрам.

Если запущена только одна программа без многопоточности, то нет, от этого не получится никакого существенного преимущества - вы одновременно разрушаете только одну вещь, кроме фоновых процессов, управляемых ОС.

...