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 BufferedImage
rấ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 BuferedImage
loạ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_ARGB
hoặ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 BufferedImages
nhanh 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 DataBuffer
từ Raster
hì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 BufferedImage
trườ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 DataBuffer
từ 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 DataBuffer
từ một BufferedImage
bứ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 setRGB
phươ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)