Максимальна обчислювальна потужність реалізації С


28

Якщо ми підемо за книгою (або будь-якою іншою версією специфікації мови, якщо вам зручніше), яку обчислювальну потужність може мати реалізація C?

Зауважте, що "реалізація C" має технічне значення: це особливе опис специфікації мови програмування C, де документально визначена поведінка. Реалізація змінного струму не повинна працювати на фактичному комп’ютері. Він повинен реалізувати всю мову, включаючи кожен об'єкт, що має представлення бітових рядків і типи, що мають розмір, визначений реалізацією.

Для цього питання немає зовнішнього сховища. Єдиний вхід / вихід, який ви можете виконати, - це getchar(для читання введення програми) та putchar(для запису програмного результату). Також недійсна будь-яка програма, яка викликає невизначене поведінку: дійсна програма повинна мати свою поведінку, визначену специфікацією C плюс опис реалізації поведінки, визначеної в додатку J (для C99). Зауважте, що виклики функцій бібліотеки, які не згадуються у стандарті, є невизначеною поведінкою.

Моя початкова реакція полягала в тому, що реалізація C - це не що інше, як обмежений автомат, оскільки він має обмеження на кількість адресируемой пам'яті (ви не можете адресувати більше sizeof(char*) * CHAR_BITбітів пам’яті, оскільки окремі адреси пам'яті повинні мати чіткі бітові шаблони при зберіганні у байтовому покажчику).

Однак я думаю, що реалізація може зробити більше, ніж це. Наскільки я можу сказати, стандарт не встановлює обмежень на глибину рекурсії. Таким чином, ви можете робити стільки рекурсивних викликів функцій, скільки вам потрібно, лише всі, окрім обмеженої кількості дзвінків, повинні використовувати registerаргументи, що не адресовані ( ). Таким чином, реалізація C, яка дозволяє довільну рекурсію і не обмежує кількість registerоб'єктів, може кодувати детерміновані автоматичні витиски.

Це правильно? Чи можете ви знайти більш потужну реалізацію C? Чи існує реалізація системи Turing?


4
@Dave: Як пояснив Жилл, здається, що ви можете мати необмежену пам’ять, але жодного способу безпосередньо не вирішити.
Jukka Suomela

2
З вашого пояснення, це здається, що будь-яка реалізація C може бути запрограмована лише на те, щоб прийняти мови, прийняті детермінованими автоматичними кнопками, які слабкіші, ніж навіть без контекстних мов. Таке спостереження, на мій погляд, мало цікавить, оскільки питання полягає в неправильному застосуванні асимптотики.
Воррен Шуді

3
Слід пам’ятати, що існує багато способів запустити «визначену реалізацією поведінку» (або «невизначену поведінку»). І взагалі, реалізація може забезпечувати, наприклад, функції бібліотеки, які забезпечують функціональність, яка не визначена в стандарті C. Усе це забезпечує «лазівки», через які ви могли отримати доступ, скажімо, до машини Тюрінга. Або навіть щось набагато сильніше, як оракул, який вирішує проблему зупинки. Дурний приклад: поведінка, визначена реалізацією підписаних цілих чисел або перетворень цілих чисел, може надати вам доступ до такого оракула.
Юкка Суомела

7
До речі, може бути хорошою ідеєю додати тег "рекреаційний" (або все, що ми використовуємо для кумедних головоломок), щоб люди не сприймали це занадто серйозно. Це, очевидно, "неправильне питання", але все-таки я вважав це забавним та інтригуючим. :)
Jukka Suomela

2
@Jukka: Гарна ідея. Наприклад, переповнення на X = запишіть X / 3 на стрічку і рухайтесь у напрямку X% 3, underflow = запускайте сигнал, відповідний символу на стрічці. Це трохи схоже на зловживання, але це безумовно в дусі мого питання. Чи можете ви написати це як відповідь? (@others: Не те, що я хочу відмовити від інших таких розумних пропозицій!)
перестань бути злим"

Відповіді:


8

unsigned charunsigned char*sizeof(unsigned char*)sizeof(unsigned char *)unsigned charUCHAR_MAXsizeof(unsigned char)101010

Програма може зберігати необмежену кількість інформації в стеку, якщо ніщо, що виділяється на стек, ніколи не приймає свою адресу ; Таким чином, можна було б мати програму С, здатну робити деякі речі, які не можна було зробити жодним обмеженим автоматом будь-якого розміру. Таким чином, навіть якщо (або, можливо, тому, що) доступ до змінних стеків значно обмежений, ніж доступ до динамічно розподілених змінних, це перетворює C з кінцевого автомата в автоматичний поштовх.

UCHAR_MAXsizeof(unsigned char)можливі послідовності знаків символів; будь-яка програма, яка створила ряд покажчиків на виділення об'єктів, що перевищують це, не могла відповідати стандарту C, якщо код коли-небудь перевіряв послідовність символів, пов'язаних з цими вказівниками . Однак у деяких випадках компілятор міг би визначити, що жоден код ніколи не збирається досліджувати послідовність символів, пов'язаних з покажчиком. Якщо кожен «char» насправді міг би містити будь-яке кінцеве ціле число, а пам’ять машини являла собою незліченно безмежну послідовність цілих чисел [з огляду на необмежену стрічкову машину Тюрінга, можна було б імітувати таку машину, хоча це було б дуже повільно], тоді дійсно можна було б зробити мову цілковою Тюрінгом.


Що б з такою машиною повернути розмір (char)?
TLW

1
@TLW: Те саме, що і будь-яка інша машина: 1. Макроси CHAR_BITS та CHAR_MAX були б дещо більш проблемними; Стандарт не допускає поняття типів, які не мають меж.
supercat

Ну, я мав на увазі CHAR_BITS, як ви сказали, вибачте.
TLW

7

За допомогою бібліотеки різьблення C11 (необов'язково) можна здійснити повну реалізацію Тьюрінга з необмеженою глибиною рекурсії.

Створення нової нитки дає другий стек; Для повноти Тьюрінга достатньо двох стеків. Один стек являє собою те, що знаходиться зліва від голови, інший стопку, що праворуч.


Але машини Тьюрінга зі стрічкою, що нескінченно просувається лише в одному напрямку, настільки ж потужні, як і машини Тьюрінга зі стрічкою, що нескінченно просувається у двох напрямках. Крім цього, планувальник може імітувати кілька потоків. У будь-якому разі, нам навіть не потрібна бібліотека для нарізки.
xamid

3

Я думаю, що це Тьюрінг завершений : ми можемо написати програму, яка імітує UTM за допомогою цього фокусу (я швидко написав код вручну, тому, ймовірно, є деякі синтаксичні помилки ... але я сподіваюся, що в логіці немає (основних) помилок :-)

  • визначте структуру, яку можна використовувати як подвійний зв'язаний список для подання стрічки
    typdef structure {
      cell_t * pred; // комірка зліва
      cell_t * succ; // комірка праворуч
      int val; // значення комірки
    } cell_t 

headБуде покажчик на cell_tструктуру

  • визначте структуру, яку можна використовувати для зберігання поточного стану та прапора
    typedef structure {
      стан стану;
      int прапор;
    } info_t 
  • потім визначте функцію єдиного циклу, яка імітує Універсальну ТМ, коли головка знаходиться між межами подвійного пов'язаного списку; коли голова потрапила на межу, встановіть прапор структури info_t (HIT_LEFT, HIT_RIGHT) і поверніть:
void simulate_UTM (cell_t * head, info_t * info) {
  в той час як (правда) {
    head-> val = UTM_nextsymbol [info-> стан, head-> val]; // символ написання
    info-> state = UTM_nextstate [info-> стан, голова-> val]; // наступний стан
    if (info-> state == HALT_STATE) {// print якщо приймає та виходить із програми
       putchar ((info-> state == ACCEPT_STATE)? '1': '0');
       вихід (0);
    }
    int move = UTM_nextmove [info-> стан, голова-> val];
    if (переміщення == MOVE_LEFT) {
      голова = голова-> перед; // рухатися ліворуч
      if (head == NULL) {info-> flag = HIT_LEFT; повернення; }
    } else {
      голова = голова-> сукк; // рухатися праворуч
      if (head == NULL) {info-> flag = HIT_RIGHT; повернення; }
    }
  } // все ще в межі ... йти далі
}
  • потім визначте рекурсивну функцію, яка спочатку викликає процедуру моделювання UTM, а потім рекурсивно викликає себе, коли стрічку потрібно розширити; коли стрічку потрібно розгорнути вгорі (HIT_RIGHT) немає проблем, коли її потрібно змістити на низ (HIT_LEFT), просто змістіть значення комірок за допомогою подвійного пов'язаного списку:
недійсний укладальник (cell_t * верх, cell_t * знизу, cell_t * head, info_t * info) {
  simulate_UTM (голова, інформація);
  cell_t newcell; // нова комірка
  newcell.pred = верх; // оновити подвійний зв'язаний список новою коміркою
  newcell.succ = NULL;
  top-> succ = & newcell;
  newcell.val = EMPTY_SYMBOL;

  перемикач (інформація-> хіт) {
    корпус HIT_RIGHT:
      укладальник (& newcell, bottom, newcell, info);
      перерву;
    корпус HIT_BOTTOM:
      cell_t * tmp = newcell;
      while (tmp-> pred! = NULL) {// значення зрушення вгору
        tmp-> val = tmp-> pred-> val;
        tmp = tmp-> pred;
      }
      tmp-> val = EMPTY_SYMBOL;
      укладальник (& newcell, bottom, bottom, info);
      перерву;
  }
}
  • початкова стрічка може бути заповнена простою рекурсивною функцією, яка створює подвійний зв'язаний список, а потім викликає stackerфункцію, коли вона читає останній символ вхідної стрічки (використовуючи readchar)
недійсна init_tape (cell_t * верх, cell_t * знизу, info_t * info) {
  cell_t newcell;
  int c = readchar ();
  if (c == END_OF_INPUT) укладальник (& top, bottom, bottom, info); // більше немає символів, почати
  newcell.pred = верх;
  if (top! = NULL) top.succ = & newcell; else bottom = & newcell;
  init_tape (& newcell, bottom, info);
}

EDIT: трохи подумавши про це, виникає проблема з покажчиками ...

якщо кожен виклик рекурсивної функції stackerможе підтримувати дійсний вказівник на змінну, визначену локально у виклику, то все добре ; інакше мій алгоритм не може підтримувати дійсний подвійний зв'язаний список необмеженої рекурсії (і в цьому випадку не бачити способу використання рекурсії для імітації необмеженого сховища з випадковим доступом).


3
stackernewcellstacker2n/sns=sizeof(cell_t)

@Gilles: ти маєш рацію (див. Мою редакцію); якщо ви обмежите глибину рекурсії, ви отримаєте кінцевий автомат
Marzio De Biasi

@MarzioDeBiasi Ні, він помиляється, оскільки посилається на конкретну реалізацію, яку стандарт не передбачає. Насправді, не існує теоретична межа глибини рекурсії в C . Вибір використання обмеженої стекової реалізації не говорить про теоретичні обмеження мови. Але Тюрінг-повнота - це теоретична межа.
xamid

0

Поки у вас є необмежений розмір стека викликів, ви можете кодувати стрічку на стеку викликів і випадково отримувати доступ до неї, перемотаючи покажчик стека, не повертаючись з функції викликів.

Редагувати : Якщо ви можете використовувати лише таран, який є кінцевим, ця конструкція більше не працює, тому дивіться нижче.

Однак дуже сумнівно, чому ваш стек може бути нескінченним, але суттєвий баран - це не так. Тому насправді я б сказав, що ви навіть не можете розпізнати всі регулярні мови, оскільки кількість штатів обмежена (якщо ви не рахуєте трюк стека-перемотування назад для використання нескінченного стека).

Я б навіть припускав, що кількість мов, які ви можете розпізнати, є кінцевою (навіть якщо самі мови можуть бути нескінченними, наприклад, a*це нормально, але b^kпрацює лише для обмеженої кількості ks).

EDIT : Це неправда, оскільки ви можете кодувати поточний стан додатковими функціями, щоб ви могли справді розпізнати ВСІ звичайні мови.

Ви, швидше за все, можете отримати всі мови типу 2 з тієї ж причини, але я не впевнений, чи зможете ви поставити як стек викликів, так і стан та стек. Але загалом, ви можете ефективно забути про таран, оскільки ви завжди можете масштабувати розмір автомата, щоб ваш алфавіт перевищував ємність барана. Отже, якби ви могли імітувати TM лише стеком, Type-2 дорівнював би типу 0, чи не так?


5
Що таке "покажчик стека"? (Зверніть увагу, що слово "стек" не відображається в стандарті C.) Моє запитання стосується C як класу формальних мов, а не реалізації C на комп'ютері (які, очевидно, є машинами з кінцевим станом). Якщо ви хочете отримати доступ до стеку викликів, ви повинні це зробити так, як це передбачено мовою. Наприклад, взявши адресу аргументів функції - але будь-яка реалізація має лише обмежену кількість адрес, що потім обмежує глибину рекурсії.
Жил "ТАК - перестань бути злим"

Я змінив свою відповідь, щоб виключити використання покажчика стека.
бітмаска

1
Я не розумію, куди ти йдеш із переглянутою відповіддю (крім зміни формули з обчислюваних функцій на розпізнані мови). Оскільки функції також мають адресу, вам потрібна досить велика реалізація, щоб реалізувати будь-яку машину з кінцевим станом. Питання полягає в тому, чи може і як реалізація C може зробити більше (скажімо, впровадити універсальну машину Тьюрінга), не покладаючись на не визначену поведінку.
Жил "ТАК - перестань бути злим"

0

Я подумав про це один раз і вирішив спробувати впровадити безконтекстну мову, використовуючи очікувану семантику; Ключовою частиною реалізації є наступна функція:

void *it;

void read_triple(void *back)
{
  if(read_a()) read_triple(&back);
  else reject();
  for(it = back; it != NULL; it = *it)
     if(!read_b()) reject();
  if(read_c()) return;
  else reject();
}

{anbncn}

Принаймні, я думаю, що це працює. Можливо, я роблю якусь принципову помилку.

Виправлена ​​версія:

void *it;

void read_triple(void *back)
{
  if(read_a()) read_triple(&back);
  else for(it = back; it != NULL; it = * (void **) it)
     if(!read_b()) reject();
  if(read_c()) return;
  else reject();
}

Ну, це не принципова помилка, але її it = *itслід замінити it = * (void **) it, як інакше *itмає тип void.
Бен Сендевен

Я був би дуже здивований , якщо подорож стек викликів як це буде визначено поведінку в С.
Radu Григора

О, це не спрацює, тому що перший 'b' призводить до відмови read_a () і, отже, викликає відхилення.
Бен Сендевен

Але легітимним є пересування стека викликів таким чином, оскільки стандарт C говорить: "Для такого об'єкта [тобто такого з автоматичним зберіганням], який не має типу масиву змінної довжини, його тривалість життя продовжується від вступу в блок з з яким він пов'язаний, поки виконання цього блоку не завершиться будь-яким способом. (Введення закритого блоку або виклик функції призупиняється, але не закінчується, виконання поточного блоку.) Якщо блок вводиться рекурсивно, новий екземпляр об'єкта створюється щоразу ". Таким чином, кожен виклик read_triple створював би новий вказівник, який можна використовувати в рекурсії.
Бен Сендевен

2
2CHAR_BITsizeof(char*)

0

Відповідно до відповіді @ supercat:

Ствердження про незавершеність C, здається, зосереджені навколо того, що окремі об'єкти повинні мати чіткі адреси, а набір адрес вважається кінцевим. Як пише @supercat

Як зазначається у запитанні, стандарт C вимагає, щоб існувало таке значення UCHAR_MAX, що кожна змінна типу без підпису char завжди матиме значення між 0 і UCHAR_MAXвключно. Він також вимагає, щоб кожен динамічно виділений об'єкт був представлений послідовністю байтів, яку можна ідентифікувати за допомогою вказівника типу непідписаний char *, і щоб була константа sizeof(unsigned char*)такою, щоб кожен вказівник цього типу був ідентифікований послідовністю sizeof(unsigned char *)значень типу без підпису char.

unsigned char*N{0,1}sizeof(unsigned char*){0,1}sizeof(unsigned char)Nsizeof(unsigned char*)Nω

На цьому етапі слід перевірити, чи стандарт C справді дозволяє це.

sizeofZ


1
Багато операцій над інтегральними типами визначаються таким чином, щоб результат був "зменшеним по модулю на один більше, ніж максимальне значення, що може бути представлене в типі результату". Як би це діяло, якщо цей максимум є нескінченним порядком?
Жил "ТАК - перестань бути злим"

@Gilles Це цікавий момент. Дійсно не зрозуміло, якою була б семантика uintptr_t p = (uintptr_t)sizeof(void*)(введення \ omega в щось, що містить непідписані цілі числа). Я не знаю. Ми можемо піти, визначивши результат 0 (або будь-яке інше число).
Олексій Б.

1
uintptr_tповинні були бути і нескінченними. Зауважте, цей тип необов’язковий, але якщо у вас є нескінченна кількість різних вказівних значень, то sizeof(void*)вони також повинні бути нескінченними, тому вони size_tповинні бути нескінченними. Моє заперечення щодо модуля скорочення не настільки очевидне - воно грає лише у випадку, якщо є перелив, але якщо ви дозволяєте нескінченних типів, вони ніколи не можуть переповнюватись. Але на захоплюючій руці кожен тип має мінімальні та максимальні значення, які, наскільки я можу сказати, означають, що вони UINT_MAX+1повинні переповнюватись.
Жил "ТАК - перестань бути злим"

Також хороший момент. Дійсно, ми отримуємо купу типів (покажчики та size_t), які повинні бути ℕ, ℤ або деяка побудова на їх основі (для size_t, якщо це буде щось на зразок ℕ ∪ {ω}). Тепер, якщо для деяких із цих типів стандарт вимагає макроса, що визначає максимальне значення (PTR_MAX або щось подібне), речі стануть волохатими. Але поки що мені вдалося фінансувати лише вимогу макросів MIN / MAX для типів, що не вказують.
Олексій Б.

Ще одна можливість дослідити - визначити обидва size_tта вказівні типи як be ∪ {ω}. Це позбавляється від проблеми min / max. Проблема із семантикою переповнення все ще залишається. Якою має бути семантика, uint x = (uint)ωмені незрозуміло. Знову ми можемо випадково прийняти 0, але це виглядає трохи некрасиво.
Олексій Б.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.