Доказ того, що мертвий код не може виявити компілятори


32

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

public class DeadCode {
  public static void main(String[] args) {
     return;
     System.out.println("This line won't print.");
  }
}

У програмі, наведеній вище, очевидно, що оператор друку ніколи не виконується через return. Компілятори іноді дають попередження або помилки щодо мертвого коду. Наприклад, вищевказаний код не компілюється на Java. Однак компілятор javac не виявить усіх випадків мертвого коду в кожній програмі. Як я можу довести, що жоден компілятор не може цього зробити?


29
Який твій досвід і в якому контексті ти будеш викладати? Щоб бути тупим, я м'яко переживаю, що вам доведеться це запитати, бачачи, як ви збираєтесь викладати. Але гарний дзвінок просять тут!
Рафаель


9
@ MichaelKjörling Виявлення мертвого коду неможливо навіть без цих міркувань.
Девід Річербі

2
BigInteger i = 0; while(isCollatzConjectureTrueFor(i)) i++; printf("Hello world\n");
користувач253751

2
@immibis Питання вимагає підтвердження того, що виявлення мертвого коду неможливо . Ви навели приклад, коли правильне виявлення мертвого коду вимагає вирішення відкритої проблеми з математики. Це не доводить, що виявлення мертвого коду неможливо .
Девід Річербі

Відповіді:


57

Все це випливає з невирішеності проблеми зупинки. Припустимо, у нас є "ідеальна" функція мертвого коду, деяка машина Turing Machine M, а також деякий рядок вводу x та процедура, яка виглядає приблизно так:

Run M on input x;
print "Finished running input";

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

Шлях цього ми обходимо шляхом "консервативного наближення". Отже, у моєму прикладі машини Turing Machine можна припустити, що запуск M on x може закінчитися, тому ми захищаємо його безпечно і не видаляємо оператор друку. У вашому прикладі ми знаємо, що незалежно від того, які функції виконують чи не зупиняються, ми не можемо дійти до цього оператора друку.

Зазвичай це робиться шляхом побудови "графіку контрольного потоку". Ми робимо спрощення припущень, таких як "кінець циклу часу підключений до початку та заяви після", навіть якщо він працює вічно або працює лише один раз і не відвідує обидва. Аналогічно, ми припускаємо, що if-заява може охопити всі її гілки, навіть якщо насправді деякі ніколи не використовуються. Такі види спрощень дозволяють нам видалити "очевидно мертвий код", як у наведеному прикладі, залишаючись при цьому можливими.

Щоб уточнити кілька плутанин у коментарях:

  1. Нітпік: для фіксованого M це завжди можна вирішити. M має бути входом

    Як говорить Рафаель, у моєму прикладі ми розглядаємо машину Тюрінга як вклад. Ідея полягає в тому, що якби у нас був ідеальний алгоритм DCE, ми змогли б побудувати фрагмент коду, який я даю для будь-якої машини Тьюрінга , і наявність DCE вирішить проблему зупинки.

  2. не переконаний. повернення як тупе твердження в прямому прямому виконанні без відгалуження не важко вирішити. (і мій компілятор говорить мені, що він здатний розібратися в цьому)

    Щодо питання njzk2 піднімає: ви абсолютно праві, у цьому випадку ви можете визначити, що після отримання повернення неможливо отримати заяву. Це тому, що це досить просто, що ми можемо описати його недосяжність, використовуючи обмеження графіка контрольного потоку (тобто немає жодних вихідних ребер із оператора повернення). Але не існує ідеального усунення мертвого коду, який виключає весь невикористаний код.

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

    Для TomášZato: це насправді не доказ, що залежить від входу. Скоріше, інтерпретуйте це як "forall". Це працює так: припустимо, у нас є досконалий алгоритм DCE. Якщо ви дасте мені довільну машину Turing M та введення x, я можу використовувати свій алгоритм DCE, щоб визначити, чи зупиняється M, побудувавши фрагмент коду вище та побачивши, чи видалено оператор друку. Ця методика залишати параметр довільним для доведення твердження forall, є загальним у математиці та логіці.

    Я не повністю розумію думку Томаша Зато про те, що код є кінцевим. Безумовно, код є кінцевим, але до всього коду, який представляє собою нескінченний набір, повинен застосовуватися досконалий алгоритм DCE. Так само, хоча сам код є скінченним, потенційні набори введення є нескінченними, як і потенційний час роботи коду.

    Щодо розгляду остаточної гілки не мертвою: це безпечно з точки зору "консервативного наближення", про яке я говорю, але недостатньо для виявлення всіх випадків мертвого коду, як цього вимагає ОП.

Розглянемо такий код:

while (true)
  print "Hello"
print "goodbye"

Зрозуміло, що ми можемо видалити, print "goodbye"не змінюючи поведінку програми. Таким чином, це мертвий код. Але якщо (true)в whileумові є інший виклик функції , то ми не знаємо, чи можемо ми його видалити чи ні, що призведе до невизначення.

Зауважте, що я не придумую цього. Це добре відомий результат в теорії компіляторів. Це обговорюється в книзі «Тигр» . (Можливо, ви зможете побачити, де про них говорять у книгах Google .


1
@ njzk2: Ми намагаємось показати, що неможливо створити елімінатор мертвого коду, який би усував увесь мертвий код, не те, що неможливо створити елімінатор мертвого коду, який виключає якийсь мертвий код. Приклад друку після повернення можна легко усунути, використовуючи методи графічного керування потоком, але не весь мертвий код можна усунути таким чином.
user2357112 підтримує Моніку

4
Ця відповідь посилається на коментарі. Коли я читаю відповідь, мені потрібно заскочити в коментарі, а потім повернутися до відповіді. Це заплутано (вдвічі, якщо ви вважаєте, що коментарі неміцні і можуть бути втрачені). Самостійну відповідь було б набагато простіше прочитати.
TRiG

1
@ TomášZato - розглянемо програму, яка збільшує змінну і перевіряє, чи є непарним ідеальним числом, що закінчується лише тоді, коли він знаходить таке число. Зрозуміло, що ця програма не залежить від зовнішнього вводу. Ви стверджуєте, що легко визначити, припиняється чи ні ця програма? nnn
Григорій Дж. Пулео

3
@ TomášZato Ви помиляєтесь із розумінням проблеми зупинки. З огляду на кінцевий машин Тьюринга , і кінцевий вхідний , що неможливо визначити , є чи нескінченно петлею під час роботи на . Я не доводив цього суворо, тому що це було доведено знову і знову, і це фундаментальний принцип інформатики. У Вікіпедії є хороший ескіз доказуx M xMxMx
липня 15:15

1
jmite, будь ласка, включіть у відповідь дійсні коментарі, щоб відповідь стояла самостійно. Потім позначте всі коментарі, які застаріли як такі, щоб ми могли прибрати. Спасибі!
Рафаель

14

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

Розглянемо наступний клас входів для ідентифікатора мертвого коду:

simulateMx(n) {
  simulate TM M on input x for n steps
  if M did halt
    return 0
  else
    return 1
}

Оскільки Mі xє виправленими, він simulateMsмає мертвий код, return 0якщо і тільки якщо Mне зупиняється x.

Це негайно дає нам зменшення від проблеми зупинки до перевірки мертвого коду: задавши TM як екземпляр, що зупиняє проблему, створіть вище програму з кодом - у неї є мертвий код, якщо і лише якщо не зупиняється самостійно код.М МMxMM

Отже, перевірка мертвого коду не обчислюється.

Якщо ви не знайомі зі скороченням як доказовим методом у цьому контексті, я рекомендую наш довідковий матеріал .


5

Простий спосіб продемонструвати подібний властивість, не заглиблюючись у деталі, - це використовувати наступну лему:

Лемма: Для будь-якого компілятора C для мови, повністю завершеної Тьюрінгом, існує функція, undecidable_but_true()яка не бере аргументів і повертає булеву істину, таким чином, що C не може передбачити, undecidable_but_true()повертається як true чи false.

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

Доведення: за теоремою Райса властивість "ця функція повертає істину" не визначається. Тому жоден алгоритм статичного аналізу не може визначити цю властивість для всіх можливих функцій.

Висновок: Враховуючи компілятор C, наступна програма містить мертвий код, який неможливо виявити:

if (!undecidable_but_true()) {
    do_stuff();
}

Примітка про Java: мова Java наказує, що компілятори відкидають певні програми, що містять недоступний код, при цьому розумно наказуючи, що цей код надається у всіх доступних точках (наприклад, потік управління в недійсній функції повинен закінчуватися returnвипискою). Мова точно визначає, як виконується недоступний аналіз коду; якби цього не було, то неможливо було б написати портативні програми. Дано програму форми

some_method () {
    <code whose continuation is unreachable>
    // is throw InternalError() needed here?
}

необхідно вказати, в яких випадках недосяжному коду повинен дотримуватися якийсь інший код, а в яких випадках не повинен дотримуватися жоден код. Приклад програми Java, що містить недоступний код, але не таким чином, щоб компілятори Java могли помітити, з'являється на Java 101:

String day_of_week(int n) {
    switch (n % 7) {
    case 0: return "Sunday";
    case 1: case -6: return "Monday";
    …
    case 6: case -1: return "Saturday";
    }
    // return or throw is required here, even though this point is unreachable
}

Зауважте, що деякі компілятори для деяких мов можуть виявити, що кінець day_of_weekнедоступний.
користувач253751

@immibis Так, наприклад, учні CS101 можуть це зробити за моїм досвідом (хоча, правда, учні CS101 не є аналізатором статичного звуку, вони зазвичай забувають про негативні випадки). Це частина мого моменту: це приклад програми з недосяжним кодом, який компілятор Java не виявить (принаймні, може попередити, але може не відхилити).
Жил "ТАК - перестань бути злим"

1
Я боюся, що формулювання леми в кращому випадку вводить в оману з відтінком неправильності. Нерозбірливість має сенс, лише якщо ви сформулюєте її у вигляді (нескінченних) наборів примірників. (Компілятор дійсно дає відповідь на кожну функцію, і ми знаємо, що вона не завжди може бути правильною, але, кажучи, що єдина невідкладна інстанція вимкнена.) Ваш абзац між лемою і доказом (що не зовсім відповідає леммі як сказано) намагається це виправити, але я думаю, що було б краще сформулювати чітко правильну лему.
Рафаель

@Raphael Uh? Ні, компілятору не потрібно давати відповіді на запитання "чи ця функція є постійною?" Для створення робочого коду не потрібно відрізняти "я не знаю" від "ні", але це не актуально, оскільки нас цікавить лише частина статичного аналізу компілятора, а не частина перекладу коду. Я не розумію, що ви вважаєте оманливим чи невірним у твердженні леми - хіба ви ставитеся до того, що я повинен написати «статичний аналізатор» замість «компілятора»?
Жил "ТАК - перестань бути злим"

Висловлювання звучить як "нерозбірливість означає, що існує екземпляр, який неможливо вирішити", що є неправильним. (Я знаю, ви цього не хочете сказати, але саме так можна прочитати необережним / послушникам, imho.)
Рафаель

3

Відповідь jmite стосується того, чи програма коли-небудь вийде з обчислення - тільки тому, що це нескінченно, я б не називав код після того, як він буде мертвим.

Однак є ще один підхід: проблема, на яку є відповідь, але вона невідома:

public void Demo()
{
  if (Chess.Evaluate(new Chessboard(), int.MaxValue) != 0)
    MessageBox.Show("Chess is unfair!");
  else
    MessageBox.Show("Chess is fair!");
}

public class chess
{
  public Int64 Evaluate(Chessboard Board, int SearchDepth)
  {
  ...
  }
}

Ця процедура , без сумніву , дійсно містить мертвий код - функція повертає відповідь , який виконує один шлях , але не інші. Вдало знайдіть це, хоча! Моя пам'ять - жоден теоретичний комп'ютер не може вирішити це протягом життя Всесвіту.

Більш детально:

У Evaluate()функції обчислює , яка сторона виграє гри в шахи , якщо обидві сторони грають відмінно (з максимальною глибиною пошуку).

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

public Int64 Evaluate(Chessboard Board, int SearchDepth)
{
  foreach (ChessMove Move in Board.GetPossibleMoves())
    {
      Chessboard NewBoard = Board.MakeMove(Move);
      if (NewBoard.Checkmate()) return int.MaxValue;
      if (NewBoard.Draw()) return 0;
      if (SearchDepth == 0) return NewBoard.Score();
      return -Evaluate(NewBoard, SearchDepth - 1);
    }
}

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

Зауважте, що глибина пошуку знаходиться в половині ходів, а не в цілому. Це нормально для такого роду рутинних методів AI, оскільки це O (x ^ n) рутина - додавання ще одного пошукового шару має великий вплив на те, скільки часу потрібно запустити.


8
Ви припускаєте, що алгоритм перевірки повинен був виконати обчислення. Поширена помилка! Ні, ви не можете взяти на себе що - небудь про те , як шашка буде працювати, в іншому випадку ви не можете спростувати його існування.
Рафаель

6
Питання вимагає підтвердження того, що неможливо виявити мертвий код. Ваш пост містить приклад випадку, коли ви підозрюєте, що важко буде виявити мертвий код. Це не є відповіддю на відповідне питання.
Девід Річербі

2
@LorenPechtel Я не знаю, але це не доказ. Дивіться також тут ; більш чіткий приклад вашого помилкового уявлення.
Рафаель

3
Якщо це допомагає, вважайте, що теоретично нічого не заважає комусь запускати свій компілятор впродовж всього життя Всесвіту; єдине обмеження - практичність. Проблема, що вирішується, є вирішуваною проблемою, навіть якщо вона знаходиться в класі складності НЕЗАДАЧА.
Псевдонім

4
Іншими словами, ця відповідь у кращому випадку є евристикою, яка має на меті показати, чому, мабуть, непросто побудувати компілятор, який виявляє весь мертвий код - але це не є доказом неможливості. Такий приклад може бути корисним як спосіб побудови інтуїції для студентів, але це не є доказом. Представляючи себе як доказ, це робить сумлінну послугу. Відповідь слід відредагувати, щоб сказати, що це приклад побудови інтуїції, але не доказ неможливості.
DW

-3

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

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

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

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


1
Питання вимагає доказ того, що неможливо виявити мертвий код. Ви не відповіли на це запитання.
Девід Річербі

Крім того, ваше твердження про те, що "компілятор може визначити, коли у вас є код, який не можна проходити в жодному сценарії компіляції", є невірним і прямо суперечить тому, що запитання вимагає довести.
Девід Річербі

@David Richerby, я думаю, ти можеш мене неправильно читати. Я не припускаю, що перевірка під час компіляції може знайти ВСЕ мертвий код, абсолютно точно. Я припускаю, що існує підмножина набору всіх мертвих кодів, які можна помітити під час компіляції. Якщо я напишу: if (true == false) {print ("щось");}, це твердження для друку буде помітно під час компіляції, щоб він був мертвим кодом. Чи не погоджуєтесь ви, що це контрприклад вашому твердженню?
dwoz

Звичайно, ви можете визначити якийсь мертвий код. Але якщо ви збираєтесь сказати "визначте, коли [у вас мертвий код]" без кваліфікації, то, для мене, це означає знайти весь мертвий код, а не лише його частину.
Девід Річербі
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.