Модульность
От: x-code  
Дата: 10.08.15 09:25
Оценка:
Добрый день! Давайте поговорим про модульность.
Можете рассказать подробно, как устроена модульность и сборка многофайловых проектов (с точки зрения компилятора) в современных компилируемых языках, типа C#, Java, Scala и т.д.?
(спрашиваю, потому что имею опыт только с С++, про который можно сказать что там модульность устроена совершенно безобразно, можно сказать что ее и нет вовсе).

Пример на C#
// файл1
namespace module1
{
    class Program
    {
        static void Main(string[] args)
        {
            module1.Program.Foo();
            module2.Class2.Foo();
        }
        public static void Foo()
        {
            System.Console.WriteLine("1.Foo");
            module2.Class2.Bar();
        }
        public static void Bar()
        {
            System.Console.WriteLine("1.Bar");
        }
    }
}
//файл2
namespace module2
{
    class Class2
    {
        public static void Foo()
        {
            System.Console.WriteLine("2.Foo");
            module1.Program.Bar();
        }

        public static void Bar()
        {
            System.Console.WriteLine("2.Bar");
        }
    }
}


аналогичный пример на java
//файл1
package module1;

public class Program {
    public static void main(String[] args) 
    {
        module1.Program.Foo();
        module2.Class2.Foo();
    }
    public static void Foo()
    {
        System.out.println("1.Foo");
        module2.Class2.Bar();
    }
    public static void Bar()
    {
        System.out.println("1.Bar");
    }
}
//файл2
package module2;

public class Class2 {
    public static void Foo()
    {
        System.out.println("2.Foo");
        module1.Program.Bar();
    }
    
    public static void Bar()
    {
        System.out.println("2.Bar");
    }
}


В обоих примерах сделано перекрестное использование классов и методов между двумя файлами.

Понятно, что от пофайловой компиляции никуда не уйти; в проекте могут быть десятки/сотни тысяч файлов, и загружать их все одновременно в память (чтобы компилятор имел доступ ко всей программе сразу) — неразумно и часто невозможно.

В С/С++ есть деление на cpp и h, которое позволяет компилятору при компиляции каждого файла видеть все определения, используемые в данном cpp файле. Поэтому, если переписать данный пример на С++, то в каждый "cpp" пришлось бы подключать оба "h" файла; и хорошо еще, если в самих "h" файлах не требуется подключение другого "h" (иногда можно решить путем включения одного "h" в другой, иногда — путем предварительных объявлений, но бывает что и это не помогает, если оба "h" используют код друг друга).

А как решается эта проблема в C# или Java? Используется двухпроходная компиляция (сначала собираем всю информацию о доступных классах/методах, кладем ее в общую для всего проекта базу данных, а затем — пофайловая компиляция с использованием этой базы)?
Или что-то еще?

Какие недостатки существуют в таких подходах и пути их устранения? (про с++ не говорим — там вся эта система одни большой недостаток) Может быть кто-то знает какие решения применяются в других языках (D, Go, Rust...)?
Re: Модульность
От: WolfHound  
Дата: 10.08.15 11:12
Оценка: 6 (2)
Здравствуйте, x-code, Вы писали:

XC>(спрашиваю, потому что имею опыт только с С++, про который можно сказать что там модульность устроена совершенно безобразно, можно сказать что ее и нет вовсе).

Там её нет.

XC>Понятно, что от пофайловой компиляции никуда не уйти; в проекте могут быть десятки/сотни тысяч файлов, и загружать их все одновременно в память (чтобы компилятор имел доступ ко всей программе сразу) — неразумно и часто невозможно.

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

XC>В С/С++ есть деление на cpp и h, которое позволяет компилятору при компиляции каждого файла видеть все определения, используемые в данном cpp файле.

По факту чуть менее чем всегда в каждый cpp тащат все объявления в проекте.

XC>А как решается эта проблема в C# или Java?

Просто поднимают весь проект в память и всё.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Re[2]: Модульность
От: x-code  
Дата: 10.08.15 12:04
Оценка:
Здравствуйте, WolfHound, Вы писали:

XC>>А как решается эта проблема в C# или Java?

WH>Просто поднимают весь проект в память и всё.

Правда? Самое простое решение, загрузить все сразу... Является ли оно самым лучшим во всех случаях?
А если распределенная (на несколько машин в сети) компиляция? Или таких задач просто не возникает?
Re[3]: Модульность
От: WolfHound  
Дата: 10.08.15 12:16
Оценка:
Здравствуйте, x-code, Вы писали:

XC>Правда? Самое простое решение, загрузить все сразу... Является ли оно самым лучшим во всех случаях?

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

XC>А если распределенная (на несколько машин в сети) компиляция? Или таких задач просто не возникает?

Оно нужно только для С++ ибо там нужно тысячи раз проделывать одну и ту же работу. Причём этой работы не оправдано много.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Re[3]: Модульность
От: Sinix  
Дата: 10.08.15 14:43
Оценка: 4 (1) +1
Здравствуйте, x-code, Вы писали:

XC>Правда? Самое простое решение, загрузить все сразу... Является ли оно самым лучшим во всех случаях?

XC>А если распределенная (на несколько машин в сети) компиляция? Или таких задач просто не возникает?
Тут есть нюанс.

Тот код, что ты привёл для шарпа и для явы к модульности никакого отношения не имеет. namespace/package — не более чем префиксы к именам типов. Т.е., одинаковые/разные неймспейсы — разницы никакой.
До тех пор, пока типы лежат внутри одного jar-а(сборки), они вполне могут ссылаться друг на друга (ну, если область видимости позволяет, конечно).


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

На практике всё немного сложнее. Например, в дотнете сборки System и System.Xml преотлично ссылаются друг на друга. Официально такая чорная магия не поддерживается (хотя в ней нет ничего сложного). Поэтому можно не заморачиваться и вернуться к теории (см предыдущий абзац).
Re[4]: Модульность
От: x-code  
Дата: 10.08.15 15:57
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Ну а когда дело доходит до разбиения кода на отдельные компоненты-сборки, всё совсем просто. По крайней мере, в теории

S>Зависимости между сборками должны укладываться в направленный граф без циклов, т.е. цикличных зависимостей между типами в разных сборках просто быть не может.

S>На практике всё немного сложнее. Например, в дотнете сборки System и System.Xml преотлично ссылаются друг на друга. Официально такая чорная магия не поддерживается (хотя в ней нет ничего сложного). Поэтому можно не заморачиваться и вернуться к теории (см предыдущий абзац).


То есть, в рамках компиляции одного проекта (приложения, библиотеки, сборки...) правильный подход — загружать все файлы-исходники в единое синтаксическое дерево и дальше работать с ним как с одним целым; а при разбиении кода на компоненты-сборки нужно осознанно запретить циклические ссылки (и вроде бы с точки зрения архитектуры это тоже будет правильно). То есть в некотором роде единицей компиляции становится не файл исходного кода, а файл проекта.
Я правильно понял?
Re[5]: Модульность
От: Sinix  
Дата: 10.08.15 16:10
Оценка:
Здравствуйте, x-code, Вы писали:

XC>есть в некотором роде единицей компиляции становится не файл исходного кода, а файл проекта.

XC>Я правильно понял?

Более-менее так. На практике есть неимоверная куча нюансов в зависимости от языка и устройства внутренностей компилятора, но я полагаю вас не это интересует
Re[6]: Модульность
От: x-code  
Дата: 10.08.15 16:13
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Более-менее так. На практике есть неимоверная куча нюансов в зависимости от языка и устройства внутренностей компилятора, но я полагаю вас не это интересует


именно это и интересует...
Re: Модульность
От: VladD2 Российская Империя www.nemerle.org
Дата: 10.08.15 22:01
Оценка: 4 (1)
Здравствуйте, x-code, Вы писали:

XC>Пример на C#

XC>
// файл1
XC>namespace module1
XC>{
XC>}
XC>//файл2
XC>namespace module2
XC>{
XC>    }
XC>}


XC>В обоих примерах сделано перекрестное использование классов и методов между двумя файлами.


В C# пространства имен к модулям отношения не имеют, как и в С++. Модульность в C# реализуется на уровне проектов/сборок (т.е. длл/ехе). Внутри проекта любой файл может видеть все до чего у него есть доступ (т.е. к публичным или internal членам).

XC>Понятно, что от пофайловой компиляции никуда не уйти; в проекте могут быть десятки/сотни тысяч файлов, и загружать их все одновременно в память (чтобы компилятор имел доступ ко всей программе сразу) — неразумно и часто невозможно.


Всегда возможно. У тебя не верные представления. Конечно там делаются оптимизации и дерево разбора никто постоянно не хранит, но вся информация о проекте C# держится в памяти во время компиляции или работы IDE. Это не так уж много, как кажется.

XC>В С/С++ есть деление на cpp и h, которое позволяет компилятору при компиляции каждого файла видеть все определения, используемые в данном cpp файле. Поэтому, если переписать данный пример на С++, то в каждый "cpp" пришлось бы подключать оба "h" файла; и хорошо еще, если в самих "h" файлах не требуется подключение другого "h" (иногда можно решить путем включения одного "h" в другой, иногда — путем предварительных объявлений, но бывает что и это не помогает, если оба "h" используют код друг друга).


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

XC>А как решается эта проблема в C# или Java? Используется двухпроходная компиляция (сначала собираем всю информацию о доступных классах/методах, кладем ее в общую для всего проекта базу данных, а затем — пофайловая компиляция с использованием этой базы)?

XC>Или что-то еще?

Она не просто двухпроходная, а много-много проходная. Но проходы делаются не по исходникам, как в стародавнии времена, а по АСТ и другим высокоуровневым структурам. По сему это не так дорого. Но память, да, тратится. Весь проект C# держится в памяти.

XC>Какие недостатки существуют в таких подходах и пути их устранения? (про с++ не говорим — там вся эта система одни большой недостаток) Может быть кто-то знает какие решения применяются в других языках (D, Go, Rust...)?


Ну, вот подход дотнета приводит к заметному расходу памяти. Но для хорошей работы IDE в память берутся даже не отдельные проекты, а целые солюшены (в майкросовстовской терминологии). Хранится все намного сложнее. Там куча кэшей и прочей лабуды. Но в итоге работает быстро и удобно.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[4]: Модульность
От: VladD2 Российская Империя www.nemerle.org
Дата: 10.08.15 22:03
Оценка:
Здравствуйте, WolfHound, Вы писали:

WH>Модулей, которые бы не влезали в память современных машин, я не видел.

WH>Все приседания, о которых ты говоришь, нужны только для старых машин, где памяти очень мало.

Ну, у РеШарпера на огромных проектах есть проблемы с памятью, так как студия ограничена 2 гигами (32-битный процесс). Так что там реализована "подкачка" данных и работа с кэшами.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[4]: Модульность
От: VladD2 Российская Империя www.nemerle.org
Дата: 10.08.15 22:07
Оценка: 38 (1)
Здравствуйте, Sinix, Вы писали:

S>Тот код, что ты привёл для шарпа и для явы к модульности никакого отношения не имеет. namespace/package — не более чем префиксы к именам типов. Т.е., одинаковые/разные неймспейсы — разницы никакой.

S>До тех пор, пока типы лежат внутри одного jar-а(сборки), они вполне могут ссылаться друг на друга (ну, если область видимости позволяет, конечно).

Для дотнета утверждения верны, для Явы — нет. В Яве модулем является каждый класс.

S>Зависимости между сборками должны укладываться в направленный граф без циклов, т.е. цикличных зависимостей между типами в разных сборках просто быть не может.


Но на практике встречаются.

В Яве все еще привольнее. У них там даже подменить типы в модулях можно, насколько я знаю.

В прочем, все равно наличие полноценной модульности с бинарным стандартом существенно упрощает жизнь создателям компиляторов и IDE.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[5]: Модульность
От: VladD2 Российская Империя www.nemerle.org
Дата: 10.08.15 22:14
Оценка:
Здравствуйте, x-code, Вы писали:

XC>То есть, в рамках компиляции одного проекта (приложения, библиотеки, сборки...) правильный подход — загружать все файлы-исходники в единое синтаксическое дерево и дальше работать с ним как с одним целым; а при разбиении кода на компоненты-сборки нужно осознанно запретить циклические ссылки (и вроде бы с точки зрения архитектуры это тоже будет правильно). То есть в некотором роде единицей компиляции становится не файл исходного кода, а файл проекта.

XC>Я правильно понял?

В дотнете все именно так с небольшими нюансами. Есть, например, дружественные сборки. Они могут "видеть" internal-типы другой сборки. Но это явно прописывается в этой другой сборке.

В яве модулем является класс (точнее класс-файл, бинарный), но допускаются перекрестные ссылки. На практике Ява-IDE вводят дополнительный (виртуальный) слой абстракции. Например, IDEA вводит понятие "модуль" объединяющей набор классов в одну логическую единицу (аналогично проекту в Visual Studio), а так же в IDEA имеется понятие "проект" — это набор модулей с которым можно работать параллельно (аналогичен понятию солюшон в Visual Studio).

IDE всегда грузит все проекты/модули в память и даже ссылки между ними отслеживает сама.

При этом делается ряд оптимизаций позволяющий уменьшить потребляемую память. В прочем, огромные проекты жрут довольно много памяти (гигабайтами).
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[5]: Модульность
От: WolfHound  
Дата: 10.08.15 22:37
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Ну, у РеШарпера на огромных проектах есть проблемы с памятью, так как студия ограничена 2 гигами (32-битный процесс). Так что там реализована "подкачка" данных и работа с кэшами.

1)Решарпер очень много памяти кушает. Компилятору нужно много меньше.
2)Не проектах, а солюшенах. А вот чтобы именно сборка в память не влезла, такого я не видел.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
Re[5]: Модульность
От: Sinclair Россия https://github.com/evilguest/
Дата: 11.08.15 04:35
Оценка:
Здравствуйте, x-code, Вы писали:

XC>То есть, в рамках компиляции одного проекта (приложения, библиотеки, сборки...) правильный подход — загружать все файлы-исходники в единое синтаксическое дерево и дальше работать с ним как с одним целым; а при разбиении кода на компоненты-сборки нужно осознанно запретить циклические ссылки (и вроде бы с точки зрения архитектуры это тоже будет правильно). То есть в некотором роде единицей компиляции становится не файл исходного кода, а файл проекта.

XC>Я правильно понял?
Да, совершенно верно. Собственно модульность (поддержка раздельной компиляции модулей) и решает проблему "как обработать 10000000 исходных файлов". То есть наше "решение" из 10М файлов на самом деле состоит из 1K модулей, каждый из которых можно скомпилировать раздельно.
При компиляции модуля не нужно компилировать или даже парсить всё то, от чего он зависит, потому что модули в приличных языках экспортируют метаданные, достаточные для компилятора, в удобном для разбора виде. Грубо говоря, компилятор С парсит исходники math столько раз, сколько .с файлов делают #import <math>. Компиляторы java, Delphi, и C# просто открывают скомпилированный math и берут описание типов и прочего оттуда. Это на порядки быстрее, чем повторно строить и типизировать AST.
(Я в курсе про precompiled headers — но это всего лишь попытка обойти архитектурное ограничение при помощи заплатки, подпёртой костылём, а не решение задачи)

В общем-то, это и есть поддержка модульности. В С/С++ в этом смысле модульности нет — мы не можем "взять определение типа A1 из модуля A", нам приходится заниматься макроподстановками.
Уйдемте отсюда, Румата! У вас слишком богатые погреба.
Re: Модульность
От: vsb Казахстан  
Дата: 11.08.15 05:28
Оценка:
Про Java расскажу. Если есть зависимость, то компилятор ищет либо .class-файл (скомпилированный байткод), либо .java-файл и читает его. Если найден java-файл, то он его тоже компилирует. Если и тот и тот, то смотрит, который новее.

Т.к. в Java имена публичных классов и имена файлов совпадают, то это работает для классов из других пакетов.

Если класс из того же пакета, это не сработает. При компиляции одного пакета надо передавать компилятору все файлы из этого пакета.
Отредактировано 11.08.2015 5:30 vsb . Предыдущая версия .
Re[2]: Модульность
От: Ikemefula Беларусь http://blogs.rsdn.org/ikemefula
Дата: 11.08.15 06:10
Оценка: +1
Здравствуйте, WolfHound, Вы писали:

WH>Думаю, ты на пару порядков завысил количество файлов в проекте.

WH>Но даже если файлов сотни тысяч нет никаких проблем поднять их все в память.

Большие системы состоят из сотен мегабай и даже гигабайт кода.

XC>>А как решается эта проблема в C# или Java?

WH>Просто поднимают весь проект в память и всё.

Интересно будет посмотреть, как в 32х битной системе поднять в память проект из гигабайтов кода.
Re[6]: Модульность
От: Ikemefula Беларусь http://blogs.rsdn.org/ikemefula
Дата: 11.08.15 06:19
Оценка:
Здравствуйте, WolfHound, Вы писали:

VD>>Ну, у РеШарпера на огромных проектах есть проблемы с памятью, так как студия ограничена 2 гигами (32-битный процесс). Так что там реализована "подкачка" данных и работа с кэшами.

WH>1)Решарпер очень много памяти кушает. Компилятору нужно много меньше.
WH>2)Не проектах, а солюшенах. А вот чтобы именно сборка в память не влезла, такого я не видел.

Солюшны, в частности, для того и нужны, что бы вручную решать проблему с памятью. Даже если компоненты приложения никто извне не использует, все равно нужно дробить на сборки
1. конская сборка, даже если не полностью компилируется джытом, жрёт драгоценное адрессное пространство сама по себе
2. если попытаться всунуть все в одну сборку, сильно вспотеешь еще до компиляции.
3. Компилятору абсолютно незачем тащить всё в память и сразу. Своп пока никто не отменял.
Re[6]: Модульность
От: VladD2 Российская Империя www.nemerle.org
Дата: 11.08.15 15:14
Оценка:
Здравствуйте, WolfHound, Вы писали:

WH>1)Решарпер очень много памяти кушает. Компилятору нужно много меньше.


Компилятор и кушает меньше. Но людям нужен и РеШарпер. Так что проблема есть, но она не столь критична. Когда появится 64-битная студия, проблема вообще исчезнет.
Есть логика намерений и логика обстоятельств, последняя всегда сильнее.
Re[7]: Модульность
От: WolfHound  
Дата: 11.08.15 16:32
Оценка:
Здравствуйте, VladD2, Вы писали:

VD>Компилятор и кушает меньше. Но людям нужен и РеШарпер. Так что проблема есть, но она не столь критична. Когда появится 64-битная студия, проблема вообще исчезнет.

По факту давно нужно вынести решарпер в отдельный процесс.
Ибо сбегать в соседний процесс немногим дороже, чем соседний поток.
... << RSDN@Home 1.2.0 alpha 5 rev. 62>>
Пусть это будет просто:
просто, как только можно,
но не проще.
(C) А. Эйнштейн
 
Подождите ...
Wait...
Пока на собственное сообщение не было ответов, его можно удалить.