Есть ли обходной путь для низкой производительности Java при обходе огромных каталогов? - PullRequest
16 голосов
/ 10 декабря 2008

Я пытаюсь обрабатывать файлы по одному, которые хранятся в сети. Чтение файлов быстро из-за буферизации не является проблемой. Проблема у меня просто перечисление каталогов в папке. У меня есть не менее 10 тыс. Файлов в папке на многие папки.

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

Ошибка записи для этого http://bugs.sun.com/view_bug.do;jsessionid=db7fcf25bcce13541c4289edeb4?bug_id=4285834 и не имеет обходной путь. Они просто говорят, что это было исправлено для JDK7.

Несколько вопросов:

  1. У кого-нибудь есть обходной путь к этому узкому месту производительности?
  2. Я пытаюсь достичь невозможного? Будет ли производительность по-прежнему плохой, даже если она просто перебирает каталоги?
  3. Могу ли я использовать бета-версии JDK7, в которых есть эта функциональность, без необходимости строить на нем весь мой проект?

Ответы [ 10 ]

7 голосов
/ 10 декабря 2008

Хотя это не очень красиво, я однажды решил эту проблему, передав по выводу dir / ls в файл перед запуском моего приложения и передав имя файла.

Если вам нужно сделать это в приложении, вы можете просто использовать system.exec (), но это создаст некоторую злобу.

Вы спросили. Первая форма будет невероятно быстрой, вторая тоже должна быть довольно быстрой.

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

РЕДАКТИРОВАТЬ:

30 минут, чтобы получить список каталогов, вау.

Меня просто поразило, что если вы используете exec (), вы можете перенаправить его стандартный поток в канал вместо записи в файл.

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

Взаимодействие может на самом деле замедлить ход событий, но, возможно, нет - вы можете попробовать.

Ух ты, я только что нашел синтаксис команды .exec для тебя и наткнулся на это, возможно, именно то, что тебе нужно (он перечисляет каталог с использованием exec и "ls" и передает результат в твою программу для обработки) : хорошая ссылка в обратном пути (Йорг предоставил в комментарии замену этого от солнца, который сломал Oracle)

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


/**
 * Note: Only use this as a last resort!  It's specific to windows and even
 * at that it's not a good solution, but it should be fast.
 * 
 * to use it, extend FileProcessor and call processFiles("...") with a list
 * of options if you want them like /s... I highly recommend /b
 * 
 * override processFile and it will be called once for each line of output.
 */
import java.io.*;

public abstract class FileProcessor
{
   public void processFiles(String dirOptions)
   {
      Process theProcess = null;
      BufferedReader inStream = null;

      // call the Hello class
      try
      {
          theProcess = Runtime.getRuntime().exec("cmd /c dir " + dirOptions);
      }
      catch(IOException e)
      {
         System.err.println("Error on exec() method");
         e.printStackTrace();  
      }

      // read from the called program's standard output stream
      try
      {
         inStream = new BufferedReader(
                                new InputStreamReader( theProcess.getInputStream() ));  
         processFile(inStream.readLine());
      }
      catch(IOException e)
      {
         System.err.println("Error on inStream.readLine()");
         e.printStackTrace();  
      }

   } // end method
   /** Override this method--it will be called once for each file */
   public abstract void processFile(String filename);


} // end class

И спасибо, донор кода в IBM

5 голосов
/ 22 октября 2013

Как насчет использования метода File.list (фильтр FilenameFilter) и реализации FilenameFilter.accept (File dir, String name) для обработки каждого файла и возврата false.

Я запустил это в Linux vm для каталога с файлами 10K +, и это заняло <10 секунд. </p>

import java.io.File;  
import java.io.FilenameFilter;

public class Temp {
    private static void processFile(File dir, String name) {
        File file = new File(dir, name);
        System.out.println("processing file " + file.getName());
    }

    private static void forEachFile(File dir) {
        String [] ignore = dir.list(new FilenameFilter() {
            public boolean accept(File dir, String name) {
                processFile(dir, name);
                return false;
            }
        });
    }

    public static void main(String[] args) {
        long before, after;
        File dot = new File(".");
        before = System.currentTimeMillis();
        forEachFile(dot);
        after = System.currentTimeMillis();
        System.out.println("after call, delta is " + (after - before));
    }  
}
4 голосов
/ 10 декабря 2008

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

Проблема здесь может заключаться не только в java (как она себя ведет, когда вы открываете этот каталог с помощью Microsoft Explorer x: \ shared) По моему опыту, это также занимает значительное время.

Вы можете изменить протокол на что-то вроде HTTP, только для получения имен файлов. Таким образом, вы можете получить список файлов через http (10 000 строк не должно быть слишком много) и позволить серверу иметь дело со списком файлов. Это будет очень быстро, так как он будет работать с локальными ресурсами (серверными)

Затем, когда у вас есть список, вы можете обработать его точно так же, как сейчас.

Ключевым моментом является наличие вспомогательного механизма на другой стороне узла.

Это возможно?

Сегодня:

File [] content = new File("X:\\remote\\dir").listFiles();

for ( File f : content ) {
    process( f );
}

Предлагаемый:

String [] content = fetchViaHttpTheListNameOf("x:\\remote\\dir");

for ( String fileName : content ) {
    process( new File( fileName ) );
}

http-сервер может быть очень маленьким и простым файлом.

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

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

Попробуйте.

3 голосов
/ 10 декабря 2008

Я сомневаюсь, что проблема связана с сообщением об ошибке, на которое вы ссылались. Здесь возникает проблема «только» использования памяти, но не обязательно скорости. Если у вас достаточно памяти, ошибка не относится к вашей проблеме.

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

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

2 голосов
/ 10 декабря 2008

Непереносимым решением было бы совершать собственные вызовы операционной системы и передавать результаты.

Для Linux

Вы можете посмотреть что-то вроде readdir . Вы можете просматривать структуру каталогов как связанный список и возвращать результаты в пакетном режиме или по отдельности.

Для Windows

В Windows поведение было бы примерно таким же, используя FindFirstFile и FindNextFile apis.

1 голос
/ 27 января 2013

Если вы работаете в Java 1.5 или 1.6, то вы можете использовать команды dir и анализировать стандартный поток вывода в Windows. В прошлом я использовал этот подход для обработки сетевых дисков, и, как правило, он был намного быстрее, чем ожидание возврата нативного метода java.io.File listFiles ().

Конечно, вызов JNI должен быть быстрее и потенциально безопаснее, чем обработка команд "dir". Следующий код JNI можно использовать для получения списка файлов / каталогов с помощью Windows API. Эта функция может быть легко преобразована в новый класс, чтобы вызывающая сторона могла постепенно извлекать пути к файлам (то есть получать один путь за раз). Например, вы можете изменить код так, чтобы FindFirstFileW вызывался в конструкторе и имел отдельный метод для вызова FindNextFileW.

JNIEXPORT jstring JNICALL Java_javaxt_io_File_GetFiles(JNIEnv *env, jclass, jstring directory)
{
    HANDLE hFind;
    try {

      //Convert jstring to wstring
        const jchar *_directory = env->GetStringChars(directory, 0);
        jsize x = env->GetStringLength(directory);
        wstring path;  //L"C:\\temp\\*";
        path.assign(_directory, _directory + x);
        env->ReleaseStringChars(directory, _directory);

        if (x<2){
            jclass exceptionClass = env->FindClass("java/lang/Exception");
            env->ThrowNew(exceptionClass, "Invalid path, less than 2 characters long.");
        }

        wstringstream ss;
        BOOL bContinue = TRUE;
        WIN32_FIND_DATAW data;
        hFind = FindFirstFileW(path.c_str(), &data);
        if (INVALID_HANDLE_VALUE == hFind){
            jclass exceptionClass = env->FindClass("java/lang/Exception");
            env->ThrowNew(exceptionClass, "FindFirstFileW returned invalid handle.");
        }


        //HANDLE hStdOut = GetStdHandle(STD_OUTPUT_HANDLE);
        //DWORD dwBytesWritten;


        // If we have no error, loop thru the files in this dir
        while (hFind && bContinue){

          /*
          //Debug Print Statment. DO NOT DELETE! cout and wcout do not print unicode correctly.
            WriteConsole(hStdOut, data.cFileName, (DWORD)_tcslen(data.cFileName), &dwBytesWritten, NULL);
            WriteConsole(hStdOut, L"\n", 1, &dwBytesWritten, NULL);
            */

          //Check if this entry is a directory
            if (data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY){
                // Make sure this dir is not . or ..
                if (wstring(data.cFileName) != L"." &&
                    wstring(data.cFileName) != L"..")
                {   
                    ss << wstring(data.cFileName) << L"\\" << L"\n";
                }
            }
            else{
                ss << wstring(data.cFileName) << L"\n";
            }
            bContinue = FindNextFileW(hFind, &data);
        }   
        FindClose(hFind); // Free the dir structure



        wstring cstr = ss.str();
        int len = cstr.size();
        //WriteConsole(hStdOut, cstr.c_str(), len, &dwBytesWritten, NULL);
        //WriteConsole(hStdOut, L"\n", 1, &dwBytesWritten, NULL);
        jchar* raw = new jchar[len];
        memcpy(raw, cstr.c_str(), len*sizeof(wchar_t));
        jstring result = env->NewString(raw, len);
        delete[] raw;
        return result;
    }
    catch(...){
        FindClose(hFind);
        jclass exceptionClass = env->FindClass("java/lang/Exception");
        env->ThrowNew(exceptionClass, "Exception occured.");
    }

    return NULL;
}

Кредит: https://sites.google.com/site/jozsefbekes/Home/windows-programming/miscellaneous-functions

Даже при таком подходе все еще есть эффективность. Если вы сериализуете путь к файлу java.io.File, это приведет к значительному снижению производительности, особенно если путь представляет файл на сетевом диске. Я понятия не имею, что Sun / Oracle делает под капотом, но если вам нужны дополнительные атрибуты файла, помимо пути к файлу (например, размер, дата модификации и т. Д.), Я обнаружил, что следующая функция JNI намного быстрее, чем создание экземпляра Java .io.File объект по сети путь.

JNIEXPORT jlongArray JNICALL Java_javaxt_io_File_GetFileAttributesEx(JNIEnv *env, jclass, jstring filename)
{   

  //Convert jstring to wstring
    const jchar *_filename = env->GetStringChars(filename, 0);
    jsize len = env->GetStringLength(filename);
    wstring path;
    path.assign(_filename, _filename + len);
    env->ReleaseStringChars(filename, _filename);


  //Get attributes
    WIN32_FILE_ATTRIBUTE_DATA fileAttrs;
    BOOL result = GetFileAttributesExW(path.c_str(), GetFileExInfoStandard, &fileAttrs);
    if (!result) {
        jclass exceptionClass = env->FindClass("java/lang/Exception");
        env->ThrowNew(exceptionClass, "Exception Occurred");
    }

  //Create an array to store the WIN32_FILE_ATTRIBUTE_DATA
    jlong buffer[6];
    buffer[0] = fileAttrs.dwFileAttributes;
    buffer[1] = date2int(fileAttrs.ftCreationTime);
    buffer[2] = date2int(fileAttrs.ftLastAccessTime);
    buffer[3] = date2int(fileAttrs.ftLastWriteTime);
    buffer[4] = fileAttrs.nFileSizeHigh;
    buffer[5] = fileAttrs.nFileSizeLow;

    jlongArray jLongArray = env->NewLongArray(6);
    env->SetLongArrayRegion(jLongArray, 0, 6, buffer);
    return jLongArray;
}

Полный рабочий пример этого подхода на основе JNI можно найти в библиотеке javaxt-core . В моих тестах с использованием Java 1.6.0_38 с хостом Windows, работающим на общем ресурсе Windows, я обнаружил, что этот подход JNI примерно в 10 раз быстрее, чем вызов java.io.File listFiles () или выписка команд dir.

1 голос
/ 10 декабря 2008

Если вам в конечном итоге понадобится обработать все файлы, то использование Iterable over String [] не даст вам никакого преимущества, так как вам все равно придется пойти и получить весь список файлов.

0 голосов
/ 01 марта 2009

Вы уверены, что это связано с Java, а не просто с общей проблемой наличия 10 тыс. Записей в одном каталоге, особенно по сети?

Вы пытались написать программу для проверки концепции, чтобы сделать то же самое в C, используя функции win32 findfirst / findnext, чтобы проверить, работает ли она быстрее?

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

Наличие 10k строк в массиве звучит как нечто, что не должно слишком обременять современную Java VM.

0 голосов
/ 01 марта 2009

Использование Iterable не означает, что файлы будут переданы вам. На самом деле это обычно наоборот. Таким образом, массив обычно быстрее, чем Iterable.

0 голосов
/ 10 декабря 2008

Интересно, почему в каталоге 10k файлов? Некоторые файловые системы плохо работают с таким количеством файлов. Существуют специфические ограничения для файловых систем, такие как максимальное количество файлов в каталоге и максимальное количество уровней в подкаталоге.

Я решаю подобную проблему с помощью итератора.

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

Я пытаюсь использовать FileUtils.iterateFiles () из Apache commons io. Но он реализует итератор, добавляя все файлы в список, а затем возвращая List.iterator (). Это очень плохо для памяти.

Поэтому я предпочитаю написать что-то вроде этого:

private static class SequentialIterator implements Iterator<File> {
    private DirectoryStack dir = null;
    private File current = null;
    private long limit;
    private FileFilter filter = null;

    public SequentialIterator(String path, long limit, FileFilter ff) {
        current = new File(path);
        this.limit = limit;
        filter = ff;
        dir = DirectoryStack.getNewStack(current);
    }

    public boolean hasNext() {
        while(walkOver());
        return isMore && (limit > count || limit < 0) && dir.getCurrent() != null;
    }

    private long count = 0;

    public File next() {
        File aux = dir.getCurrent();
        dir.advancePostition();
        count++;
        return aux;
    }

    private boolean walkOver() {
        if (dir.isOutOfDirListRange()) {
            if (dir.isCantGoParent()) {
                isMore = false;
                return false;
            } else {
                dir.goToParent();
                dir.advancePostition();
                return true;
            }
        } else {
            if (dir.isCurrentDirectory()) {
                if (dir.isDirectoryEmpty()) {
                    dir.advancePostition();
                } else {
                    dir.goIntoDir();
                }
                return true;
            } else {
                if (filter.accept(dir.getCurrent())) {
                    return false;
                } else {
                    dir.advancePostition();
                    return true;
                }
            }
        }
    }

    private boolean isMore = true;

    public void remove() {
        throw new UnsupportedOperationException();
    }

}

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

И DirectoryStack:

public class DirectoryStack {
    private class Element{
        private File files[] = null;
        private int currentPointer;
        public Element(File current) {
            currentPointer = 0;
            if (current.exists()) {
                if(current.isDirectory()){
                    files = current.listFiles();
                    Set<File> set = new TreeSet<File>();
                    for (int i = 0; i < files.length; i++) {
                        File file = files[i];
                        set.add(file);
                    }
                    set.toArray(files);
                }else{
                    throw new IllegalArgumentException("File current must be directory");
                }
            } else {
                throw new IllegalArgumentException("File current not exist");
            }

        }
        public String toString(){
            return "current="+getCurrent().toString();
        }
        public int getCurrentPointer() {
            return currentPointer;
        }
        public void setCurrentPointer(int currentPointer) {
            this.currentPointer = currentPointer;
        }
        public File[] getFiles() {
            return files;
        }
        public File getCurrent(){
            File ret = null;
            try{
                ret = getFiles()[getCurrentPointer()];
            }catch (Exception e){
            }
            return ret;
        }
        public boolean isDirectoryEmpty(){
            return !(getFiles().length>0);
        }
        public Element advancePointer(){
            setCurrentPointer(getCurrentPointer()+1);
            return this;
        }
    }
    private DirectoryStack(File first){
        getStack().push(new Element(first));
    }
    public static DirectoryStack getNewStack(File first){
        return new DirectoryStack(first);
    }
    public String toString(){
        String ret = "stack:\n";
        int i = 0;
        for (Element elem : stack) {
            ret += "nivel " + i++ + elem.toString()+"\n";
        }
        return ret;
    }
    private Stack<Element> stack=null;
    private Stack<Element> getStack(){
        if(stack==null){
            stack = new Stack<Element>();
        }
        return stack;
    }
    public File getCurrent(){
        return getStack().peek().getCurrent();
    }
    public boolean isDirectoryEmpty(){
        return getStack().peek().isDirectoryEmpty();
    }
    public DirectoryStack downLevel(){
        getStack().pop();
        return this;
    }
    public DirectoryStack goToParent(){
        return downLevel();
    }
    public DirectoryStack goIntoDir(){
        return upLevel();
    }
    public DirectoryStack upLevel(){
        if(isCurrentNotNull())
            getStack().push(new Element(getCurrent()));
        return this;
    }
    public DirectoryStack advancePostition(){
        getStack().peek().advancePointer();
        return this;
    }
    public File[] peekDirectory(){
        return getStack().peek().getFiles();
    }
    public boolean isLastFileOfDirectory(){
        return getStack().peek().getFiles().length <= getStack().peek().getCurrentPointer();
    }
    public boolean gotMoreLevels() {
        return getStack().size()>0;
    }
    public boolean gotMoreInCurrentLevel() {
        return getStack().peek().getFiles().length > getStack().peek().getCurrentPointer()+1;
    }
    public boolean isRoot() {
        return !(getStack().size()>1);
    }
    public boolean isCurrentNotNull() {
        if(!getStack().isEmpty()){
            int currentPointer = getStack().peek().getCurrentPointer();
            int maxFiles = getStack().peek().getFiles().length;
            return currentPointer < maxFiles;
        }else{
            return false;
        }
    }
    public boolean isCurrentDirectory() {
        return getStack().peek().getCurrent().isDirectory();
    }
    public boolean isLastFromDirList() {
        return getStack().peek().getCurrentPointer() == (getStack().peek().getFiles().length-1);
    }
    public boolean isCantGoParent() {
        return !(getStack().size()>1);
    }
    public boolean isOutOfDirListRange() {
        return getStack().peek().getFiles().length <= getStack().peek().getCurrentPointer();
    }

}
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...