Java mảng đơn lựa chọn tốt nhất để truy cập pixel để thao tác?


7

Tôi chỉ xem hướng dẫn này https://www.youtube.com/watch?v=HwUnMy_pR6A và anh chàng (có vẻ khá có năng lực) đang sử dụng một mảng duy nhất để lưu trữ và truy cập các pixel của anh ấy sẽ được hiển thị hình ảnh.

Tôi đã tự hỏi nếu đây thực sự là cách tốt nhất để làm điều này. Sự thay thế của Multi-Array có thêm một con trỏ, nhưng Mảng có O (1) để truy cập từng chỉ mục và tính toán chỉ mục trong một mảng dường như chỉ cần một phép cộng và một phép nhân cho mỗi pixel.

Và nếu Multi-Mảng thực sự xấu, bạn có thể sử dụng thứ gì đó với Băm để tránh các phép toán cộng và nhân đó không?

EDIT: đây là mã của anh ấy ...

public class Screen {
private int width, height;
public int[] pixels;

public Screen(int width, int height) {
    this.width = width;
    this.height = height;

    // creating array the size of one index/int for every pixel
    // single array has better performance than multi-array
    pixels = new int[width * height];
}

public void render() {
    for (int y = 0; y < height; y++) {
        for (int x = 0; x < width; x++) {
            pixels[x + y * width] = 0xff00ff;
        }
    }
}
}

Đọc câu trả lời cho câu hỏi này, nó có thể giúp đỡ. stackoverflow.com/questions/7051100/2d-array-vs-1d-array
Savlon

Câu trả lời:


9

Nói về "sự lựa chọn tốt nhất" luôn luôn khó khăn, miễn là bạn không chỉ định nhiệm vụ mà bạn dự định thực hiện một cách chi tiết.

Nhưng đây là một số khía cạnh để xem xét. Trước hết: Java không phải C. Quản lý bộ nhớ hoàn toàn khác nhau, vì vậy người ta phải rất cẩn thận khi cố gắng áp dụng những hiểu biết sâu sắc từ môi trường lập trình này sang môi trường lập trình khác.

Ví dụ: tuyên bố của bạn:

Sự thay thế của Multi-Array có thêm một con trỏ, nhưng Mảng có O (1) để truy cập từng chỉ mục và tính toán chỉ mục trong một mảng dường như chỉ cần một phép cộng và một phép nhân cho mỗi pixel.

Như đã chỉ ra trong một trong những câu trả lời được liên kết: "Mảng 2D" trong C chủ yếu là sự thuận tiện về mặt cú pháp để giải quyết một khối bộ nhớ (1D). Vì vậy, khi bạn viết một cái gì đó như

array[y][x] = 123; // y comes first because y corresponds to rows and x to columns

trong C, thì điều này thực sự có thể là hiệu quả đối với

array[x+columns*y] = 123;

Vì vậy, đối số của việc lưu một phép nhân ít nhất là nghi vấn ở đây. Một trường hợp đặc biệt ở đây là khi bạn thực sự có một mảng các con trỏ , trong đó mỗi hàng được phân bổ thủ công malloc. Sau đó, lược đồ địa chỉ là khác nhau (và trên thực tế, điều này có thể được coi là gần gũi hơn với những gì Java làm).

Ngoài ra, Java là một ngôn ngữ có trình biên dịch đúng lúc, khiến việc so sánh ở mức độ của các hướng dẫn đơn lẻ (như phép nhân) rất khó khăn. Bạn hầu như không bao giờ biết JIT làm gì với mã của bạn. Nhưng trong mọi trường hợp, bạn có thể cho rằng nó sẽ làm cho nó nhanh hơn và sử dụng các thủ thuật khó nhất mà bạn có thể (không) tưởng tượng để làm như vậy.


Liên quan đến chiến lược chung để sử dụng mảng 1D cho pixel, có một lý do để thực hiện điều này trong Java (và từ việc lướt nhanh qua video được liên kết, lý do này dường như được áp dụng ở đây): Mảng 1D có thể được chuyển vào một cách thuận tiện BufferedImage.

Đây là một lý do từ quan điểm "thuận tiện" thuần túy. Nhưng khi nói về hiệu suất , có những khía cạnh khác quan trọng hơn nhiều so với một vài phép nhân. Những cái đầu tiên tôi nghĩ đến là

  • Hiệu ứng bộ nhớ cache
  • Chi tiết thực hiện

Hai ví dụ cho thấy chúng có thể ảnh hưởng đến hiệu suất như thế nào:

Hiệu ứng bộ nhớ cache

Đối với ví dụ đầu tiên, bạn có thể xem xét ví dụ sau: Nó cho thấy sự khác biệt đáng kể về hiệu suất giữa việc truy cập pixel với

for (int x = 0; x < width; x++) {
    for (int y = 0; y < height; y++) {
        pixels[x + y * width] = ...
    }
}

và với

for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
        pixels[x + y * width] = ...
    }
}

Tóm tắt trong một chương trình thử nghiệm nhỏ:

public class PixelTestButNoBenchmark
{
    public static void main(String[] args)
    {
        long before = 0;
        long after = 0;
        double readTime = 0;
        double writeTime = 0;

        for (int size = 1000; size<=10000; size+=1000)
        {
            Screen s0 = new Screen(size, size);

            before = System.nanoTime();
            s0.writeRowMajor(size);
            after = System.nanoTime();
            writeTime = (after-before)/1e6;

            before = System.nanoTime();
            int r0 = s0.readRowMajor();
            after = System.nanoTime();
            readTime = (after-before)/1e6;

            System.out.printf(
                "Size %6d Row    major " +
                "write: %8.2f " +
                "read: %8.2f " +
                "result %d\n", size, writeTime, readTime, r0);

            Screen s1 = new Screen(size, size);

            before = System.nanoTime();
            s1.writeColumnMajor(size);
            after = System.nanoTime();
            writeTime = (after-before)/1e6;

            before = System.nanoTime();
            int r1 = s1.readColumnMajor();
            after = System.nanoTime();
            readTime = (after-before)/1e6;

            System.out.printf(
                "Size %6d Column major " +
                "write: %8.2f " +
                "read: %8.2f " +
                "result %d\n", size, writeTime, readTime, r1);

        }
    }
}

class Screen
{
    private int width, height;
    public int[] pixels;

    public Screen(int width, int height)
    {
        this.width = width;
        this.height = height;
        pixels = new int[width * height];
    }

    public void writeRowMajor(int value)
    {
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                pixels[x + y * width] = value;
            }
        }
    }

    public void writeColumnMajor(int value)
    {
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                pixels[x + y * width] = value;
            }
        }
    }

    public int readRowMajor()
    {
        int result = 0;
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                result += pixels[x + y * width];
            }
        }
        return result;
    }

    public int readColumnMajor()
    {
        int result = 0;
        for (int x = 0; x < width; x++)
        {
            for (int y = 0; y < height; y++)
            {
                result += pixels[x + y * width];
            }
        }
        return result;
    }
}

Các chi tiết sẽ khác nhau, và phụ thuộc nhiều vào hệ thống đích (và kích thước bộ đệm của nó), vì vậy đây KHÔNG phải là "điểm chuẩn", nhưng cho thấy có thể có sự khác biệt lớn. Trên hệ thống của tôi, một thứ tự cường độ, ví dụ.

Size   7000 Row    major write:    46,73 read:    28,09 result -597383680
Size   7000 Column major write:   528,46 read:   500,53 result -597383680

Ảnh hưởng thứ hai có thể còn khó khăn hơn:

Chi tiết thực hiện

Lớp Java BufferedImagerất thuận tiện và các hoạt động sử dụng lớp này có thể nhanh chóng.

Điều kiện đầu tiên cho điều này là loại hình ảnh phải "phù hợp" với hệ thống đích. Đặc biệt, tôi chưa bao giờ gặp phải một hệ thống trong đó các BuferedImageloại được hỗ trợ bởi int[]các mảng không phải là nhanh nhất. Điều đó có nghĩa là bạn phải luôn đảm bảo sử dụng hình ảnh của loại TYPE_INT_ARGBhoặc TYPE_INT_RGB.

Điều kiện thứ hai là tinh tế hơn. Các kỹ sư tại Sun / Oracle đã sử dụng một số thủ thuật khó chịu để vẽ tranh BufferedImagesnhanh nhất có thể. Một trong những tối ưu hóa này là họ phát hiện xem hình ảnh có thể được giữ hoàn toàn trong VRAM hay không hoặc liệu họ có phải đồng bộ hóa hình ảnh với nội dung của một mảng tồn tại trong thế giới Java hay không. Một hình ảnh có thể được giữ trong VRAM được gọi là hình ảnh "được quản lý". Và có một số hoạt động phá hủy "sự quản lý" của một hình ảnh. Một trong những thao tác này là lấy DataBuffertừ Rasterhình ảnh.

Một lần nữa, một chương trình mà tôi đã cố gắng chứng minh hiệu quả. Nó tạo ra hai BufferedImagetrường hợp. Trên một trong số đó, nó có được DataBuffer, và mặt khác, nó không. Lưu ý rằng đây cũng KHÔNG phải là "điểm chuẩn" và nên được thực hiện với một hạt muối khổng lồ . Nhưng nó cho thấy việc có được DataBuffertừ một hình ảnh có thể làm giảm đáng kể hiệu suất vẽ.

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.image.BufferedImage;
import java.awt.image.DataBuffer;
import java.awt.image.DataBufferInt;
import java.beans.Transient;
import java.util.Random;

import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.SwingUtilities;


public class ImagePaintingTestButNoBenchmark
{
    public static void main(String[] args)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                createAndShowGUI();
            }
        });
    }

    private static void createAndShowGUI()
    {
        JFrame f = new JFrame();
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        int size = 1200;
        BufferedImage managedBufferedImage = createImage(size);
        BufferedImage unmanagedBufferedImage = createImage(size);
        makeUnmanaged(unmanagedBufferedImage);

        final ImagePaintingPanel p = new ImagePaintingPanel(
            managedBufferedImage, unmanagedBufferedImage);
        f.add(p);

        startRepaintingThread(p);

        f.pack();
        f.setLocationRelativeTo(null);
        f.setVisible(true);
    }

    private static void startRepaintingThread(final JComponent c)
    {
        Thread t = new Thread(new Runnable()
        {
            @Override
            public void run()
            {
                while (true)
                {
                    c.repaint();
                    try
                    {
                        Thread.sleep(50);
                    }
                    catch (InterruptedException e)
                    {
                        Thread.currentThread().interrupt();
                    }
                }
            }
        });
        t.setDaemon(true);
        t.start();
    }

    private static BufferedImage createImage(int size)
    {
        BufferedImage bufferedImage = 
            new BufferedImage(size, size, BufferedImage.TYPE_INT_ARGB);
        Graphics2D g = bufferedImage.createGraphics();
        Random r = new Random(0);
        for (int i=0; i<size; i++)
        {
            int x = r.nextInt(size);
            int y = r.nextInt(size);
            int w = r.nextInt(size);
            int h = r.nextInt(size);
            int c0 = r.nextInt(256);
            int c1 = r.nextInt(256);
            int c2 = r.nextInt(256);
            g.setColor(new Color(c0,c1,c2));
            g.fillRect(x, y, w, h);
        }
        g.dispose();
        return bufferedImage;
    }

    private static void makeUnmanaged(BufferedImage bufferedImage)
    {
        DataBuffer dataBuffer = bufferedImage.getRaster().getDataBuffer();
        DataBufferInt dataBufferInt = (DataBufferInt)dataBuffer;
        int data[] = dataBufferInt.getData();
        System.out.println("Unmanaged "+data[0]);
    }

}


class ImagePaintingPanel extends JPanel
{
    private final BufferedImage managedBufferedImage;
    private final BufferedImage unmanagedBufferedImage;

    ImagePaintingPanel(
        BufferedImage managedBufferedImage,
        BufferedImage unmanagedBufferedImage)
    {
        this.managedBufferedImage = managedBufferedImage;
        this.unmanagedBufferedImage = unmanagedBufferedImage;
    }

    @Override
    @Transient
    public Dimension getPreferredSize()
    {
        return new Dimension(
            managedBufferedImage.getWidth()+
            unmanagedBufferedImage.getWidth(),
            Math.max(
                managedBufferedImage.getHeight(), 
                unmanagedBufferedImage.getHeight()));
    }

    @Override
    protected void paintComponent(Graphics g)
    {
        super.paintComponent(g);

        long before = 0;
        long after = 0;

        before = System.nanoTime();
        g.drawImage(managedBufferedImage, 0, 0, null);
        after = System.nanoTime();
        double managedDuration = (after-before)/1e6;

        before = System.nanoTime();
        g.drawImage(unmanagedBufferedImage, 
            managedBufferedImage.getWidth(), 0, null);
        after = System.nanoTime();
        double unmanagedDuration = (after-before)/1e6;

        System.out.printf("Managed   %10.5fms\n", managedDuration);
        System.out.printf("Unanaged  %10.5fms\n", unmanagedDuration);
    }

}

Các tác động thực tế ở đây có lẽ sẽ phụ thuộc nhiều hơn vào phần cứng (và phiên bản VM!), Nhưng trên hệ thống của tôi, nó in

Managed      0,05151ms
Unanaged     6,27663ms

điều đó có nghĩa là việc có được DataBuffertừ một BufferedImagebức tranh có thể làm chậm bức tranh với hệ số hơn 100 .

(Một bên: Để tránh sự chậm lại này, người ta có thể sử dụng setRGBphương pháp, như trong

image.setRGB(0, 0, w, h, pixels, 0, w);

nhưng tất nhiên, điều này liên quan đến một số sao chép. Vì vậy, cuối cùng, có một sự đánh đổi giữa tốc độ thao tác các pixel và tốc độ vẽ hình ảnh)


1

Theo kinh nghiệm của tôi với mảng Java, tôi đã thấy những cải thiện đáng kể về tốc độ bằng cách sử dụng mảng 1D. Kết quả của bạn có thể khác nhau, vì vậy điều quan trọng là bạn phải thực hiện một số thử nghiệm của riêng mình.

Hãy xem xét rằng việc sử dụng chính của mảng này của bạn về cơ bản sẽ giống như cách dữ liệu sẽ được lưu trữ trong RAM. Điều này làm cho việc cấp phát bộ nhớ của mảng 1D có lợi hơn. Một mảng đa chiều cũng sẽ yêu cầu tính toán lập chỉ mục, chúng chỉ được thực hiện ở phía sau hậu trường.

Với mức độ trừu tượng chính xác, bạn có thể dễ dàng trao đổi giữa hai phương thức và thực hiện một số bài kiểm tra hiệu suất của riêng bạn.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.