В большинстве случаев копирование файлов действительно медленнее перемещения -- это зависит от устройства файловой системы и, в меньшей степени, от технологии носителя, помимо того, что прикладные программы перемещения файлов должны быть написаны корректно, используя потенциал предоставленного им интерфейса.
Для выяснения, почему это так, можно взглянуть на устройство типичной UNIX-овой файловой системы (в Windows отдельные моменты могут отличаться, но общие принципы, как минимум в контексте причин бОльшей скорости перемещения, примерно те же). Собственно, минимально достаточное для понимания ответа на вопрос (но без понимания ответов на многие сходные вопросы) описано в "минимальный рассказ" в хвосте "более детального".
Более детальный рассказ:
Отдельный экземпляр такой ФС, занимающий отдельный раздел на диске (информация о местоположении и размере которого (обычно адреса начального и конечного секторов) хранится в свою очередь в загрузочном нулевом секторе в случае MBR), в упрощенном варианте выглядит так:
| суперблок | карта свободных блоков | таблица inodes | блоки данных файлов и каталогов |
Иногда эта схема располагается последовательно по росту номеров секторов, иногда дублируется в рамках отдельных групп цилиндров, в нашем вопросе не суть важно.
Под "блоком" понимается логическая единица хранения данных в файловой системе, кратная по размеру сектору жесткого диска (с которыми оперирует драйвер и контроллер), выбор его размера -- отдельная тема.
Суперблок -- он обычно хранит метаинформацию о файловой системе, как минимум это размеры ее разделов (адреса можно не хранить, т.к. они расположены непрерывно), штампы даты и времени (например, создания, последнего монтирования/чтения/записи) и тип ФС.
Карта свободных блоков -- структура данных, отслеживающая незанятые блоки данных, обычно либо битовая карта (с постоянным размером, но большей сложностью в нахождении групп блоков нужных размеров), либо связный список (наоборот, потому может продолжаться в области данных).
Таблица inodes (индексных дескрипторов) -- последовательность записей, каждая из которых однозначно идентифицирует файл в терминах ОС.
ИД обычно хранит тип файла (обычный файл, каталог, символическая ссылка (ярлык), устройство, сокет, именованный канал и пр.), его размер в байтах, число жестких ссылок, права доступа и идентификаторы владельца и группы, а также три даты, и, конечно, адреса блоков данных файла (для больших файлов они не влазят в inode-запись (которая должна быть константной для O(1) времени доступа к заданному ИД), потому используется косвенная адресация с задействованием блоков данных).
Замечу, что он НЕ хранит имя файла (хотя иное теоретически не противоречит быстрому перемещению).
Наконец, область данных. Это, собственно, свалка блоков содержимого разных файлов вперемешку (отслеживаются для данного файла они из таблицы inode, а не по соседству, так как мы не знаем размера файла заранее, и потому файлы не хранятся непрерывно).
Уровень хаоса тут может быть понижен при разных улучшениях файловых систем, например, попытках записывать непрерывные блоки или периодической дефрагментации, что в свою очередь еще и позволяет оптимизировать адресную информацию (например, храня начало и размер последовательности в ИД), но не суть.
Минимальный рассказ (продолжение):
Здесь важно, что каждый каталог -- это тоже файл, который обычно хранит записи о входящих в него файлах (но не сами данные файлов) в формате <номер ИД> -- <имя файла> -- <прочее>. Если номер ИД == 0, запись пуста (даже если имя написано, показывая, что когда-то в каталог входил файл, либо просто случайный мусор), и это все хранится в области данных, отслеживаемой из таблицы ИД.
Гибкость описанной структуры каталога, кстати, еще и позволяет строить форматы ИД и каталога независимо друг от друга -- первый обычно более-менее стандартный, а вторые бывают самые разные, в простейшем случае это 2 байта записи номера ИД и 14 байт записи имени файла (кончающейся нулями), например
| 0x0045 | 'f' 'r' 'e' 'i' 0 0 0 0 0 0 0 0 0 0 | -- запись файла "frei"с ИД 69. Попав на 69-ю запись таблицы ИД, мы можем прочитать адреса данных файла и далее сами данные. В каталоге этих данных нет.
В более развитых структурах каталогов (под-ФС, фактически), вроде Fast File System из BSD, для снятия ограничения на 14 байт в имени файла, добавляются поля длины имени и длины всей записи (которая всегда >= имени), но принцип номер_ИД-ИМЯ остается.
--
Таким образом, чтобы перенести файл, нам достаточно всего лишь удалить запись из исходного каталога (пометив 0м ИД) и ввести ее в целевой каталог, обращаясь при этом к ИД самых каталогов за адресами их содержимого (этих записей).
И, помимо этого и не так важно в нашем контексте, изменить при этом ИД самих каталогов (уменьшив размер первого и увеличив второго, + обновив даты посл. изменения). В отдельном случае (когда текущие записи целевого занимали блок данных до конца придется выделить один дополнительный блок данных и вставить там первую запись, но это все равно не приводит к особой задержке).
Для копирования же файлов нам нужно скопировать все данные старого файла (используя карту свободных блоков) в новые места (именно здесь и происходят основные временные затраты), и далее выделить новый ИД и заполнить его соотв. адресной и прочей информацией -- так мы создали новый файл с тем же именем, фактически, и, наконец, ввести это имя файла и этот новый номер ИД в целевой каталог, обновив еще и его ИД (ради обновления размера и даты посл. изменения, ну и ИДы старых обновить ради изменения времени последнего чтения, но это уже нюансы и зависит от дополнительных полей в ИД).
--
В принципе, ничто не мешает реализовать копирование файлов так, чтобы НЕ копировать при этом блоки данных.
Точно по аналогии с копированием процессов в памяти при выполнении -- там используется copy-on-write ("копирование при записи") подход, согласно которому страницы памяти не копируются, а вместо этого помечаются как "только для чтения" + "copy-on-write". Тогда для чтения оба процесса используют одни и те же страницы, а вот если кто-то из них захочет записать туда что-то (испортив картину с т.з. другого процесса, имеющего иной взгляд на содержимое страниц и не видящего изменений), только тогда происходит реальное копирование этой одной страницы.
Имхо, это будет очень не удобно уже в силу намного более медленного доступа к диску, чем к памяти, так как при каждой отдельной правке содержимого какого-то из файлов (которая может происходит в самых разных местах, к тому же, а в памяти доступ обычно идет к соседним данным и коду) придется регулярно обращаться к карте блоков и выделять новый блок, тогда как обычно проще выделить их разом.
--
Отдельный интересный момент, вытекающий из описанного -- разница в скорости видна только при использовании одного и того же экземпляра ФС. А вот если у нас разные разделы, то переписать номер ИД в другую ФС уже нельзя, т.к. он по-прежнему будет индексировать таблицу ИД первой ФС, потому там неизбежно реальное копирование.
Желающие могут поэкспериментировать, попробовав перенести >1Gb файлик (под Linux/Windows/Mac OS X/BSD/любой современной ОС) в рамках одного и того же раздела (должно быть почти мгновенно) и далее между разными разделами -- если раздел всего один, можно подключить флешку. Если бы было простое перемещение записи каталога, даже с учетом медленной скорости USB, процесс все равно был бы почти мгновенный.
А в случае с копированием и там, и там обычно довольно длительно.
--
Ну и, конечно, такое быстрое перемещение будет только в случае предоставления соответствующего интерфейса от ОС к программам, и при нормальном написании самой программы, которая может попросту не использовать предоставляемый ОС потенциал. В UNIX например помимо open(), creat(), read() и write(), достаточных для копирования, есть системные вызовы link() и unlink(), которые позволяют соответственно добавить связь с файлом и удалить ее, посредством как раз ввода записи в каталог или удаления ее, без манипуляций с блоками данных.
И программа перемещения файлов mv может быть написана, в паршивом варианте, через создание файла копии, чтение всего содержимого исходного файла в него и затем удаление исходного, а может, в более грамотном, через создание связи в целевом каталоге посредством link() и далее удалении старой через unlink().
Здесь правда отдельный нюанс, не связанный напрямую с проблемой, что link() должен предшествовать unlink(), т.к. иначе (даже если программа вычленит номер ИД файла через stat(), чтобы не потерять его, здесь речь совершенно не об этом) unlink() может, удаляя запись, глянуть в указанный в ней ИД и увидеть, что у файла больше нет жестких ссылок (как раз этих самых связей), и тогда решить, что надо освободить все его блоки и реально удалить содержимое файла с диска (он-то не знает, что далее кто-то будет еще создавать связи на этот ИД).
Даже здесь, конечно, удаления содержимого (переписывания его нулями) может не быть, а удалится лишь inode + обновится карта свободных блоков, и изощренными системными утилитами можно в принципе вернуть содержимое, но это, конечно, ментальный онанизм -- программа должна создавать новую связь, а только потом уничтожать старую.